Skip to main content
The board is one of the two ways a team coordinates — the durable, broadcast one. Where delegation is a private point-to-point hand-off between two sessions, the board is shared state: progress, ownership, blockers, and cross-member notes live in one place the coordinator and every member read from and write to. The board is the part of a team that outlives it. A coordinator’s transcript scrolls away; the card that says who holds what, and why it is stuck, does not — it is a row in Postgres, one board per agent, and it is still there for the next session, for you, and for the dashboard.

Enabling it

Agent config
multiagent.board is a boolean — there is nothing to configure, because the statuses are fixed. From the CLI it is --board on vetta team set. When it is false the two board tools are simply not constructed, and the team has delegation only.
A board never has to be created explicitly. It is minted on first use, keyed one per agent, so the first read or write on a brand-new team gets a board and every later one gets the same board. That is also why every board reference accepts the owning agent’s agt_ id — a board with no cards has never told anyone its own brd_ id.There is a create call — POST /v1/boards, client.board.create, vetta board create — and because there is one board per agent it is an ensure, not a create that can conflict: 201 when that call minted the row, 200 when it found one. Use it when you want the board’s brd_ id before any card exists; you never have to.

Four statuses, fixed

todo, doing, blocked, done. They are not configurable columns, and that is deliberate: a per-team column list would make the board tools’ contract depend on the agent’s config, so board_write could not name a legal status without first reading the board, and no two teams’ transcripts would be comparable. The reason a card is stuck is carried as data, not as a lane. A blocked card lists the cards it waits on in blocked_by; a blocked card with an empty list is stuck on the world. When every card in a blocked_by list reaches done, the waiting card is promoted back to todo automatically on the next read — a team must not stall on a blocker that has already cleared.

The model

  • Card — a unit of work: { id, board_id, title, body, status, assignee, blocked_by, created_at, updated_at }. assignee is a roster member’s name, not an id — that is what a coordinator writes in a tool call and what a member recognises as itself. null is unclaimed.
  • Comment — the team’s channel on a card: { id, card_id, author, body, created_at }. Any thread can add one.

The tools

When a team has a board enabled, Vetta injects two built-in tools into every session on that team — the coordinator’s and every member’s alike.
On claude_code and hermes the two tools arrive by a different road. Those harnesses run a CLI as a process in a micro-VM on its own toolset, and are given a session-scoped tool endpoint to call ours over. The board works the same from either side — a member on one of them reads its card and closes it — and your tools policy governs both tools by the same names. See Harness capabilities.

board_read

Lists the board’s cards, or reads one card in full. It answers with the board’s brd_ id alongside the cards, which is how a model learns the id of a board it has never written to. The list form omits every card’s body and comments on purpose: a board is read many times in a burst, and every card’s prose would then be re-sent on every later turn of the run. Ask for one card by id when you actually need to read it.

board_write

One tool, three operations, chosen by op.
operation
Requires title. Takes body, status, assignee and blocked_by; a card defaults to todo, unassigned and unblocked. Card ids are derived, not minted — from the board, the writing session and the title — so a turn retried after a partial write collides with the row it already wrote and the retry is a no-op. The result says created: false when that happened, rather than leaving a duplicate card behind.
operation
A status move, and only a status move. Requires card_id and status; an optional body is recorded as the note on the move. A title, an assignee or a blocked_by change through this tool is refused by name (unsupported_update) rather than accepted and silently dropped — those are set when the card is created, and a person can still change them through the API.
operation
Requires card_id and body. A comment on a card that does not exist is a refusal, never an orphan row.

A move can be refused, and that is normal

Two members move cards blind to each other, so the move is a compare-and-set and a lost race comes back as a named tool result rather than a lost update. Nothing here throws.
On a comment’s author. It is never supplied — it is derived, on both paths. Through the tool, the author recorded is the writing session’s ses_ id: the runtime knows exactly which session wrote it and does not ask the model to name itself. Through the HTTP route, it is the usr_ or key_ principal behind the key, and sending an author field is a 400. The prefix is what tells a reader an agent’s comment from a person’s.author is therefore not the same namespace as assignee, which is a roster member’s name. Treat author as “which principal wrote this”, not as something you can match against an assignee.

How it complements delegation

The proven pattern is both together: the coordinator puts the plan on the board as one card per unit of work, then delegates each card. A member moves its own card to done before it answers, and a member’s board write wakes a parked coordinator — so the board is also how a member says something urgent without finishing first.

The API

A board is addressed by its brd_ id or by the agt_ id of the agent that owns it. Reads take agents:read and writes take agents:write: a board belongs to an agent, and the scope grammar is fixed. One board per agent — unique (org_id, agent_id) — so POST /v1/boards is really an ensure: the second call is not a conflict, it is the same board, and the status code is the only difference. A comment’s author is derived from the caller and cannot be supplied: a person’s comment is signed with their usr_ or key_ principal, a member’s with the ses_ id of the thread that wrote it. The prefix is what lets a reader tell an agent’s comment from a person’s. There is no delete route, and there should not be one — a finished card is the record of what the team was asked to do. vetta board done moves a card to done. From the CLI, that is vetta board; from the client, client.board.
Board spend — the tokens members spend reading and writing it — is metered against the budget like any other call.

Back to the team overview

The two mechanisms, the coordinator model, rosters, and how context and budget flow.