Agent catalog & docs.md
Per-agent markdown documentation served at /<route>/docs, plus a built-in catalog endpoint that returns every agent the caller can reach with its @callable RPC surface.
docs.md
Drop a docs.md file next to agent.ts
and ayjnt build embeds the markdown into the worker
as a string literal. Hitting <routePath>/docs
returns it with content-type: text/markdown.
# InventoryAgent
Owns the stock counters. One Durable Object instance per warehouse.
## Callable methods
- `decrement(sku, qty)` — atomic decrement; throws on insufficient stock.
- `reset()` — wipes back to seed values.
## State shape
```ts
type State = { stock: Record<string, number> };
```curl http://localhost:8787/inventory/docs
# # InventoryAgent
#
# Owns the stock counters. .../<route>/docs runs through every middleware
that protects the agent. /admin/reports/docs is 403 forbidden for callers that don't pass agents/admin/middleware.ts — the docs are gated
the same way the API is.
docs — /<route>/docs is always
the docs route. Names like docs-prod or docs1 are fine.
@callable methods
The catalog lists methods marked with
Cloudflare's @callable() decorator from "agents" — the same decorator that makes a
method invocable from a browser client via agent.stub.method(...). One marker drives both
browser-callability and catalog discoverability:
import { Agent, callable } from "agents";
export default class InventoryAgent extends Agent<Env, State> {
/** Decrement stock for a SKU. Throws on insufficient stock. */
@callable({ description: "Decrement stock for a SKU." })
async decrement(sku: string, qty: number): Promise<number> {
/* ... */
}
/** Internal helper — NOT in the catalog (no decorator). */
private _seed(): void { /* ... */ }
}Description precedence in the catalog:
-
@callable({ description: "…" })— the decorator's option (wins). -
First prose line of the JSDoc immediately above the
method (fallback when the decorator has no
description). -
null.
Long-form JSDoc stays available for editor hover regardless of which line ends up in the catalog. Methods without the decorator stay private to the class and don't appear in the catalog.
Legacy JSDoc tag
A /** @callable */ JSDoc tag still works as a catalog-only fallback for the rare case where you
want a method listed in the catalog but NOT exposed over
WebSocket — typically an agent-to-agent RPC method you want
discoverable without making it browser-callable:
/**
* Re-seed the notes from a snapshot. Agent-to-agent RPC only —
* listed in the catalog but not browser-callable.
* @callable
*/
async reseed(snapshot: Note[]): Promise<void> { /* ... */ }Most code shouldn't need it — if a method is part of your public surface, decorate it.
- Parameter list must fit on one line.
-
Return-type annotation may not contain a top-level
{— object literal return types aren't supported. -
Decorator detection doesn't follow import aliases.
Write
@callable(...)literally, not@cb(...)after renaming the import. - Decorator args with nested parens (e.g. an arrow function inside the options object) confuse the non-greedy capture — stick to plain object literals.
The /__ayjnt/catalog endpoint
The framework reserves GET /__ayjnt/catalog. It
returns a JSON tree of every agent the caller can reach,
along with each agent's @callable methods, hasApp/hasDocs flags, and (when
docs exist) a docsUrl.
{
"version": 1,
"agents": [
{
"agentId": "inventory",
"className": "InventoryAgent",
"routePath": "/inventory",
"hasApp": false,
"hasDocs": true,
"isMcp": false,
"callables": [
{
"name": "decrement",
"params": "sku: string, qty: number",
"returnType": "Promise<number>",
"description": "Decrement stock for a SKU. Throws on insufficient stock."
},
{
"name": "reset",
"params": "",
"returnType": "Promise<void>",
"description": "Reset stock to the initial values."
}
],
"docsUrl": "/inventory/docs"
}
]
}Filtered by access
For each route, the framework runs the agent's middleware
chain against the incoming request. If any middleware
short-circuits with a non-2xx response, that agent is
hidden from the result. So an admin gate that returns 403 without a bearer token causes every agent
under /admin to disappear from the catalog for
anonymous callers.
The probe inherits the request headers, so a single Authorization: Bearer ... header that unlocks /admin/* unlocks every admin agent in one round
of probes.
Rendering the catalog as a UI
See examples/catalog for an agent with an app.tsx that fetches /__ayjnt/catalog and renders the tree, with a
bearer-token field that lets you watch admin agents appear
and disappear in real time.