Producer guide — publish your app's capabilities #
You're a producer if your app does things: saves, subscribes, shares, annotates, opens. This guide takes you from "my app has features" to "other apps and agents can discover and route to those features from a user's repo."
New to the model? Read the overview first. Want an agent to do the analysis for you? See the agent guide.
There are two jobs:
- Publish capability records (once, in your app's repo) — describe what you can do.
- Write usage records (per user, in their repo) — leave the footprint that lets consumers discover you at all.
1. Publish your capabilities #
Step 1 — enumerate your write/read surfaces #
List everything a user can do through your app. For each, you'll fill in the
triple plus delivery. Ask:
- verb — is it
share,save,subscribe,annotate,open, or something new? Don't force everything into "share." - subject — what does it act on? A bare URL (
uri)? A record of a given type (at-uri+of)? An account (did)? List all accepted shapes — it's a union. - delivery — the key decision, below.
Step 2 — choose delivery for each surface #
This is the field that decides whether a capability actually works. Pick by asking where the canonical data lives:
| If… | delivery |
You must… |
|---|---|---|
| your app just reads a primitive the user already has in their repo | passive |
nothing else — no produces, no scope |
| your app's data lives in the user's PDS as a record you read | repo |
list produces (the NSID you write) and a scope |
| your app's canonical store is your own backend (a PDS write won't reach you) | service |
give an XRPC endpoint (https://host/xrpc/<nsid>); add serviceId (your DID-doc service fragment) so consumers can reach it by service proxying |
The classic mistake: marking a backend-canonical save as
repoorpassive. A record written to the user's repo never reaches your backend, so the capability looks free and silently fails. Useservice.
Step 3 — write the record as JSON #
A capability is just a JSON record matching the
lexicon — $type and createdAt
are filled in for you. Full field reference: capability-spec.md.
{
"name": "My Reader",
"icon": "https://myreader.app/icon.png",
"description": "Subscribe to a feed or account so new items appear in your reading list. Use to follow a source over time; not for saving a single article.",
"verb": "subscribe",
"subject": [
{ "kind": "uri" },
{ "kind": "did", "as": "account" }
],
"produces": ["app.myreader.subscription"],
"delivery": "repo",
"scope": "include:app.myreader.subscribe"
}
Write description as if a planner will read it: what it does, when to use it,
what it won't do. Tool-calling quality is dominated by description quality, and
the same text feeds a human picker.
See tools/examples/ for one record per delivery type and a
multi-capability file.
Step 4 — validate and write #
You'll need an app password for your app's account (on Bluesky: Settings → Privacy and security → App passwords — never your main password). The tooling is zero-dependency Node 18+ — no install.
cd tools
# Always dry-run first: validates and prints the exact write, but writes nothing.
node write-capability.mjs examples/subscribe.capability.json --dry-run \
--identifier myreader.app
# Write for real (capability records go in YOUR app's repo):
PDS_IDENTIFIER=myreader.app PDS_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
node write-capability.mjs my-subscribe.capability.json
Prefer to be walked through it? node write-capability-interactive.mjs prompts
for each field, saves a reusable JSON record, shows a dry-run, and writes on
confirmation.
Publish your whole surface at once with a { "capabilities": [...] } file — see
examples/all.capabilities.json.
Step 5 — make the lexicon resolvable (optional, for live discovery) #
Capability records live in your repo and resolve today. For the lexicon NSID
to resolve on the open network, the authority domain (here at-intent.dev) must
publish the lexicon per atproto lexicon resolution. Until then, consumers fetch
records directly by collection NSID from your PDS — which the resolver already
does. Nothing blocks you from publishing records now.
2. Write usage records (the footprint) #
A capability nobody can discover is invisible. When a person uses your app, write
a dev.at-intent.usage record into their repo so consumers find you:
{
"$type": "dev.at-intent.usage",
"app": "did:web:myreader.app",
"config": { "defaultCategory": "tech" },
"lastSeenAt": "2026-06-26T12:00:00Z",
"createdAt": "2026-03-01T00:00:00Z"
}
- You already have write access to the user's repo (they authed your app), so you can create this during normal use.
- Dedupe by
app— one usage record per handler is enough. - Bump
lastSeenAtwhen they use the app again, so consumers can age out stale handlers (default TTL ~180 days). configis your private per-user blob (a default collection, a category) — shape is yours.
For manual/testing creation (writes into the user's repo, so auth as the user):
node write-usage.mjs --app did:web:myreader.app \
--config '{"defaultCategory":"tech"}' --dry-run --identifier alice.bsky.social