Agent guide — author AT Intents capability records for a repo #
Audience: an AI coding agent (Claude Code or similar) working inside an app's repository. A developer has said something like:
"Let's make an AT Intents capability record — see https://github.com/<...>/at-intents for context."
This document is your instruction set. Your job: scan the repo, figure out what the app does, write capability record file(s) as JSON, and tell the developer the exact CLI command to publish them. You do not publish records yourself (that needs the app's credentials) and you do not need to run anything that writes.
If you can fetch additional context, read overview.md and
capability-spec.md in this repo. The essentials are
inlined below so you can work from this file alone.
The model in 60 seconds #
AT Intents lets an app declare what it can do so other apps and agents can
discover it from a user's repo. Each capability is a (verb, subject, handler)
triple plus a delivery method, stored as a dev.at-intent.capability
record in the app's own repo.
- verb —
sharesavesubscribeannotateopen(open set; pick the closest, or coin one and flag it to the developer). - subject — what it acts on, a union of shapes:
uri(a bare web/feed URL),at-uri(a record, optionallyofa lexicon type),did(an account). - delivery —
passive(you read a record already in the user's repo),repo(the consumer writes a record into the user's repo that you read), orservice(the consumer must call your XRPC endpoint; a PDS write won't reach you). This is the field most likely to be wrong — get it right. Aserviceendpoint is an XRPC method (https://host/xrpc/<nsid>); see service shape.
Your procedure #
Step 1 — confirm scope with the developer #
Briefly state what you're about to do and ask one question if anything is ambiguous: "I'll scan the repo for user-facing actions and draft AT Intents capability specs. Should I cover all of them or a specific feature?" Then proceed.
Step 2 — scan the repo for what the app does #
You are looking for user-facing actions and the data each one touches. Gather evidence; don't guess. High-signal places to look:
App identity (for name / icon / the app DID):
package.jsonname/description,README*, site<title>/og:tags,public//static/for an icon,manifest.json, marketing copy.- The app's atproto identity: a
did:web:<domain>, a configured handle, OAuthclient-metadata.json, or.well-known/atproto-did. The capability records live in this app's repo, so the DID is the app's own.
Lexicons the app defines or writes (for subject / produces):
lexicons/**/*.json(files with"lexicon": 1) — record types this app owns.- atproto write calls: grep for
com.atproto.repo.createRecord,putRecord,applyWrites,.create(,agent.com.atproto.repo, or an SDK wrapper. Thecollection/$typeargument is aproducesNSID. - Record types it reads but doesn't own (e.g.
community.lexicon.bookmarks.bookmark,app.bsky.feed.post) → candidatepassivesubjects.
Endpoints / backends (to decide delivery):
- XRPC routes or HTTP handlers (
/xrpc/<nsid>, an API router, server routes, serverless functions) — aserviceendpoint. - A non-PDS canonical store (a database — D1, Postgres, KV; an external API) that
user actions write to. If the action's source of truth is a backend, not the
user's PDS, it's
delivery: service. - Note whether the route is already XRPC-shaped (
/xrpc/<nsid>, params in the query string,{ error, message }errors). If the action's endpoint is a plain REST/RPC route (POST /api/save, a GraphQL field, a?action=handler), flag it — see below.
The actions themselves (for verb / subject):
- UI: buttons/menus labeled save, subscribe/follow, share/post, highlight/annotate, open/view. Share targets, intent handlers, "add to…" flows.
- Functions/routes named
save,subscribe,share,post,bookmark,highlight,follow,import.
Step 3 — map each action to a capability #
For every distinct user-facing action, decide the triple + delivery using this table:
| Question | Field | How to answer from evidence |
|---|---|---|
| What's the action? | verb |
save / subscribe / share / annotate / open — closest match. |
| What can you act on? | subject[] |
A raw URL → {kind: uri}. A record you accept → {kind: at-uri, of: <nsid>}. An account → {kind: did, as: account}. List every accepted shape. |
| Where does the result live? | delivery |
In the user's PDS as a record you write/read → repo. In your backend → service. You only read an existing repo record → passive. |
| (repo) what record? | produces |
The NSID(s) from the createRecord calls. |
| (repo) what permission? | scope |
include:<your.permission.set.nsid> — ask the developer if unknown; leave a clear TODO. |
| (service) what URL? | endpoint |
The XRPC method URL (https://host/xrpc/<nsid>) that performs it. If the real route isn't XRPC-shaped, see below. |
| (service) DID-doc fragment? | serviceId |
The app's service-endpoint fragment id in its DID document (e.g. chirp_api), if it publishes one. Lets consumers reach the method by service proxying (atproto-proxy: <did>#<serviceId>) instead of a direct cross-origin call. Leave a TODO if unknown. |
| Extra inputs? | input[] |
A note, title, target collection — name + kind. |
The delivery decision is the one that silently breaks if wrong. A "save"
whose data lands in your own database is service, not repo — a PDS write
would never reach your backend, so a consumer would think it saved when it
didn't. When the evidence is ambiguous, write your best guess and call it out in
the summary for the developer to confirm.
Write description for a planner and a human: what it does, when to use it,
what it won't do (≤300 graphemes). This text drives both UI pickers and agent
tool-calling, so it matters.
Service must be XRPC-shaped #
A delivery: service endpoint is an XRPC method — an absolute URL whose
path is /xrpc/<nsid>, with the subject and inputs carried as XRPC parameters
(scalars in the query string, blobs in the request body) and errors returned as
{ error, message } JSON. The verb picks the HTTP method: open → GET query,
every other verb → POST procedure. (Full rules:
service wire contract.)
If the app's real endpoint isn't XRPC-shaped — don't quietly paper over it.
A POST /api/save, a GraphQL mutation, a ?action=save handler, or any plain
REST/RPC route can't be invoked by a consumer through this contract. When you
find one backing a service capability:
- Write the capability with the XRPC endpoint it should have
(
https://host/xrpc/<app.nsid.verb>) and mark itTODO— don't invent a route that doesn't exist yet and present it as real. - Suggest the app expose an XRPC method for that action — either a thin
/xrpc/<nsid>wrapper in front of the existing handler, or by moving the route onto the app's XRPC server. Spell out the small delta (path →/xrpc/<nsid>, read params from the query string, return{ error, message }on failure). - Call it out in the Step 6 summary as a required change before the
servicecapability will actually work, not just a style nit.
Getting this right is what keeps a service capability honest: a non-XRPC route
publishes fine but no consumer can route to it, so it "looks like it worked" while
being unreachable — the same silent-failure trap as a mislabeled repo.
Step 4 — write the record file(s) #
Create one JSON file in the repo holding the capability record(s) — a single
object, or { "capabilities": [ ... ] } for several. It's the literal
dev.at-intent.capability shape; $type and createdAt are filled in by the
writer, so you can omit them. Recommended path: at-intent/capabilities.json (or
alongside the app's lexicons). Any _-prefixed field (e.g. _comment) is
ignored, so use it for review notes.
{
"_comment": "Drafted by an agent from repo analysis. Review delivery/scope/endpoint, then publish with the CLI below.",
"capabilities": [
{
"name": "<app display name>",
"icon": "<https icon url, if found>",
"description": "<what it does, when to use it, what it won't do>",
"verb": "<save|subscribe|share|annotate|open>",
"subject": [
{ "kind": "<uri|at-uri|did>" }
],
"delivery": "<passive|repo|service>",
"_delivery_note": "Keep ONLY the fields for the delivery you chose:",
"_passive": "you read an existing repo record — no produces/scope/endpoint",
"_repo": "consumer writes a record you read — set produces + scope",
"produces": ["<nsid you write — repo only>"],
"scope": "include:<your.permission.set> TODO confirm — repo only",
"_service": "consumer calls your backend — set endpoint (+ serviceId for proxying) instead",
"endpoint": "<https url / xrpc route> TODO confirm — service only",
"serviceId": "<DID-doc service fragment, e.g. app_api> optional — service only, enables proxying"
}
]
}
Fill it from your evidence and delete the _* hint fields and any
delivery-fields that don't apply. Mark every value you guessed with a TODO
note (a _todo field, or call it out in your summary).
Step 5 — tell the developer how to publish #
Do not write records yourself. Give the developer this, adjusting the file path and identifier (the tooling is zero-dependency Node 18+ — no install):
cd tools
# 1) Dry-run — validates and prints the exact write, writes nothing:
node write-capability.mjs <path>/capabilities.json --dry-run --identifier <app-handle>
# 2) Publish to the APP's repo (use an app password, never the main password):
PDS_IDENTIFIER=<app-handle> PDS_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
node write-capability.mjs <path>/capabilities.json
Also mention the interactive builder as an alternative:
node write-capability-interactive.mjs.
Step 6 — summarize for review #
End with a short report:
- Capabilities drafted — one line each:
verb+subject+delivery. - Assumptions & TODOs — every value you guessed, especially
delivery,scope, andendpoint, with where the evidence was thin. Flag anyserviceaction whose real route isn't XRPC-shaped and recommend exposing an/xrpc/<nsid>method (see Service must be XRPC-shaped) — call it a blocker, since the capability can't be routed to until then. - Usage records — remind the developer that discovery also requires writing
a
dev.at-intent.usagerecord into each user's repo when they use the app (see producers.md); that's app code, not a one-time publish. - Next command — the dry-run line from Step 5.
Guardrails #
- Don't invent NSIDs, endpoints, scopes, or a DID. If the repo doesn't show
it, leave a
TODOand ask. A wrongproduces/endpointis worse than a blank one. - A
serviceendpoint must be XRPC (/xrpc/<nsid>). If the app's real route isn't, recommend exposing one rather than pointingendpointat a non-XRPC URL (see Service must be XRPC-shaped). - A capability may only describe its own app's records/endpoints — never another app's collections. (This is also a security rule: descriptors are untrusted input to consumer agents.)
- Don't publish anything. Your deliverable is JSON record files + the command. The
human runs the write, ideally
--dry-runfirst. - Prefer fewer, correct capabilities over many speculative ones. One verb you verified beats five you inferred.