Lexicon schemas drive code generation, request validation, and client typing across the AT Protocol stack.
Installing Lexicons with lex
Our SDKs each install a lex tool that lets you generate a type-safe client that knows which parameters each AT endpoint expects.
TypeScript
First, install the @atproto/lex package:
npm install -g @atproto/lex
This provides the lex command, which you can use to install Lexicons into a local project:
lex install app.bsky.feed.post app.bsky.feed.like
This creates:
lexicons.json- manifest tracking installed Lexicons and their versions (CIDs)lexicons/- directory containing the Lexicon JSON files
Finally, generate TypeScript schemas from the installed Lexicons:
lex build
This writes TypeScript files to ./src/lexicons. Once you've run lex build, calling a method is a typed client.call against the generated definition:
import { Client } from '@atproto/lex'
import * as app from './lexicons/app.js'
// Create an unauthenticated client instance
const client = new Client('https://public.api.bsky.app')
// Start making requests using generated schemas
const response = await client.call(app.bsky.actor.getProfile, {
actor: 'pfrazee.com',
})
For more guidance on working with lex, refer to the readme.
XRPC Methods
A Lexicon whose main definition is a method describes one of three kinds of XRPC call:
query— a read, served over HTTPGET, parameters in the query string. See Reading Data for end-to-end examples.procedure— a side-effectful call, served over HTTPPOST, input as a JSON body (or another MIME type if declared). See Writing Data.subscription— a server-pushed WebSocket stream with DAG-CBOR frames, used for the firehose.
Each method is reachable at /xrpc/<nsid> on whatever service implements it — app.bsky.actor.getProfile lives at https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile. The NSID is the URL path.
Parameters, input body, and output are all type-checked at compile time and validated at runtime against the Lexicon shape.
Using the .internal TLD
NSIDs use reverse DNS lookup — app.bsky.* resolves under bsky.app, com.atproto.* under atproto.com. But for service-to-service calls between components of the same operator, there's no public schema to publish or resolve.
For those, you can use NSIDs prefixed with internal., taking advantage of .internal being a reserved TLD that can never be registered. For example, the reference implementation includes internal.bsky.actor.getProfiles, a variant of app.bsky.actor.getProfiles.
This gives you:
- Built-in SDK exclusion. The codegen filter in
@atproto/lextreats*.internal.*as the canonical exclude pattern, so internal endpoints never appear in your published client SDK. - All of the XRPC machinery. Same codegen, typed handlers, request/response validation, inter-service auth, and
/xrpc/<nsid>routing, but without committing the endpoint to your public protocol surface.
Use internal.* whenever an XRPC method is plumbing for your own infrastructure: optimizations specific to one caller, admin or operational queries, or anything you wouldn't want a third-party implementer to feel obligated to support.