brd_ id or by the agt_ id of the agent that owns it. That is not a convenience: a board with no cards has never told anyone its id, so the agent id is the only reference a caller can have on a team that has not written a card yet.
The four statuses are fixed — todo, doing, blocked, done — and are not configurable columns. The reason a card is stuck rides in blocked_by, as data rather than as a lane.
A BoardCard is { id, object, board_id, title, body, note, status, assignee, blocked_by, woken_session_id, reported_status, wake_count, created_at, updated_at }. assignee is a roster member’s name, or null. woken_session_id, reported_status and wake_count are the board’s own bookkeeping, readable and never written by a client: woken_session_id is the ses_ id of the session the board started for the assignee (null until it has), reported_status is the terminal state — done or blocked — the owner was last told about (null while that report is still owed), and wake_count is how many sessions the board has started for the card (at most three).
create
POST /v1/boards. Ensures the agent’s board. There is one board per agent, so a second call is not a conflict — it answers the same board, and the status code is the only difference: 201 when this call minted it, 200 when it found one.
get
GET /v1/boards/{id}. One board — its brd_ id, the agt_ id of its owner, and cards, which is how many stand in each of the four columns. boardRef is either spelling.
A Board is { id, object, agent_id, cards: { todo, doing, blocked, done }, created_at }. The counts are a real aggregate, not the length of a page: a page of cards is truncated and cannot state one.
cards.list
GET /v1/boards/{id}/cards. boardRef is a brd_ id or an agt_ id.
cards.ofAgent
GET /v1/agents/{id}/board. The same cards, reached from the agent, in column order. Creates the board on first read, so it answers an empty page rather than a 404 for a team that has not started.
cards.create
POST /v1/boards/{id}/cards. CardCreate is { title, body?, status?, assignee?, blocked_by?, key? }; everything but the title has a default — todo, unassigned, unblocked.
key (up to 128 characters) makes the create idempotent on its board: the same key always answers the same card, 201 when this call minted it and 200 when it found one, so a seed that runs twice leaves one card, not two. Without a key every call is a new card. Creating with an assignee is a hand-over — see cards.assign for what that starts.
cards.get
GET /v1/boards/{id}/cards/{cid}.
cards.update
PATCH /v1/boards/{id}/cards/{cid}. Every field is optional, so a move is { status: "doing" } and nothing else. assignee: null unclaims the card; omitting the key leaves the current assignee alone.
This is an operator’s override and is deliberately more powerful than the board_write tool an agent calls: it outranks the claim guards that stop two members stepping on each other, and it is the only way to retitle a card.
One rule it does not outrank: { status: "done" } needs a note — a note on the call or one already on the card — and is refused 409 state_conflict (missing_note) otherwise. A done with nothing written is a card the next reader cannot use.
cards.assign
PATCH /v1/boards/{id}/cards/{cid} with { assignee } and nothing else. assignee is a roster member’s name; null takes the card back.
Assigning is how work starts. A todo card with an assignee and no open blocker starts a session of that teammate on the next tick, and the card records it as woken_session_id. If that session ends without the card reaching done, the board parks the card as blocked — a hand-over is never silently lost. Unassigning wakes nobody.
cards.comments
GET /v1/boards/{id}/cards/{cid}/comments. A card’s comments, oldest first — they are a conversation, not a feed, so they are not reversed. A cardId that is not on the named board answers 404, the same guard cards.get sits behind.
cards.comment
POST /v1/boards/{id}/cards/{cid}/comments. There is no author field: the server signs the comment with the principal behind your key, so author comes back as your usr_ or key_ id. A member commenting through board_write is signed with the ses_ id of its own thread, which is how a reader tells an agent’s comment from a person’s.