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: what the team is trying to do, who holds what, what is stuck and why, written where every seated agent — and you, and the dashboard — can read it. The board is the part of a team that outlives it. A transcript scrolls away; the card that says who holds what, and why it is stuck, does not. It is a row in the database, and it is still there for the next session, for you, and for the dashboard.

One board, owned by the coordinator, seated by roster

A board belongs to one agent — the coordinator, the CEO of a provisioned company — and its current roster says who else sits on it. Turn it on with multiagent.board:
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. Every session of an agent named on that roster is seated on the owner’s board, whether or not the member’s own config says anything about teams. A session’s board is resolved on every call: its own agent’s board when that agent’s multiagent.board is true; otherwise the board of the owner whose current roster names it; otherwise the company board; otherwise none, and the two tools are not published to it. Two owners naming one agent is not refused — the earlier-created owner’s board wins, and a roster that names an agent always beats the company board. The company board seats everybody, and nobody configures it. The board owned by your company’s CEO is the company board: every agent in the company is seated on it the moment it is hired, with no roster to write and no board: true to set, and an agent you delete leaves it. A roster stays what it always was — the agents this one may delegate to — so a company board and a coordinator’s own board can both exist without one standing for the other.
On the board, everyone is a name. A card’s assignee, and the claim a doing card holds, are the agent’s name — what a coordinator writes in a tool call and what a member recognises as itself. A card assigned to writer is claimable by the agent named writer from any of its sessions. A comment’s author, by contrast, is the ses_ id of the session that wrote it: the card says who holds it and the thread says which run said what.
A board never has to be created explicitly. It is minted on first use, one per agent: the owner’s first board_read or board_write, or the first board route addressed by its agt_ id, gets a board, and every later one gets the same board. Members are seated on it from then on. 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: 201 when that call minted the row, 200 when it found one. A template that declares tasks uses it to seed the CEO’s board before the crew starts.

The card lifecycle

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.
  • todo — on the board, nobody is on it. Assigned or not.
  • doing — a teammate has claimed it. Claiming is the only move that takes a card: the card’s assignee becomes the claimer’s name, and a teammate holds one doing card per board at a time.
  • blocked — waiting. The reason is data, not a lane: blocked_by lists the cards it waits on, and a blocked card with an empty list is stuck on the world — a question only the owner or a person can answer.
  • done — terminal. A card in done is never re-opened by a move; make a new card instead. done needs a note: a body on the move, or one already on the card’s note. A finished card with nothing written on it is a card the next reader cannot use.
When every card in a blocked_by list reaches done, the waiting card is healed back to todo — on the next read, and on every tick of the board’s clock, so a team never stalls on a blocker that has already cleared.

Assigning a card starts the teammate on it

A card is how work is handed over, and assignment is the wake. A card that is todo, has an assignee, has no open blocker, whose assignee holds nothing else in flight on that board and that has not already been woken is due. Once a minute, the board’s clock sweeps every due card and starts one ordinary top-level session of the assignee — the same start as creating a session yourself, on the agent’s own budget — and records the new session’s id in the card’s woken_session_id. The session’s metadata.board is { card_id, board_id }. The message that session opens with is the card, not a brief:
The trigger is a column on the card, not an event, so every door reaches it the same way: a coordinator’s assign, an operator’s PATCH, a template’s tasks seed, and the healer’s blocked → todo. It is bounded on purpose:
What the woken session is. It is a top-level session of the rostered agent — a member of the crew, seated on the owner’s board by roster, not a delegated thread of the coordinator’s session. It runs on the assignee’s own budget, is not something the coordinator parks on, and answers to the board rather than to a caller. See Coordinator for how this differs from send_to_agent.

A schedule arrives with its card already claimed

Not every card is one a person or a coordinator wrote. When a deployment fires and its agent is seated on this board, the fire opens the schedule’s standing card — Recurring: and the first line of the prompt — and claims it before the session starts. There is one such card per schedule and every fire reuses it, so a daily schedule is one row on the board with its runs as comments under it, not one row a day. The clock claims that card itself, which is why the board’s own wake never doubles it: the card is doing from the moment it is opened, and a doing card is not due. A fire that ends without closing its card is parked like any other — moved to blocked for the owner — and the next fire re-opens the same card rather than leaving the schedule stuck behind it.

When a woken session ends unfinished

A session started on a card is expected to move it to done or blocked before it stops. If the session in woken_session_id has finished — terminal, or idle on end_turn — and the card is still todo or doing, the next tick parks the card: it moves to blocked with an empty blocked_by and the system comments the session id and its stop reason —
A card left in flight by a run that stopped is never re-woken in a loop; it is put in front of the owner instead. The same happens when the wake could not start at all — the assignee out of budget, say — so the clock does not spend a start on it every minute. A card the member did move in the meantime is left alone.

The owner is told what changed

A card whose status is done or blocked and differs from its reported_status is unreported. The tick groups every unreported card by board and starts one session of the board’s owner per board with the list — one digest, however many cards changed — then stamps each card’s reported_status with what was told, so a later change is unreported again.
That digest is how the coordinator re-enters the work: read each card, decide what comes next, assign it. An owner that cannot be started — not hosted, or its budget refuses — costs that board its digest for the tick; the cards are stamped anyway rather than re-told every minute, and they are still on the board they always were.

The model

  • Card — a unit of work: { id, board_id, title, body, note, status, assignee, blocked_by, woken_session_id, reported_status, wake_count, created_at, updated_at }. body is the brief; note is what the holder wrote back.
    • assignee is an agent’s name, not an id. null is unclaimed.
    • blocked_by is the crd_ ids it waits on, already stripped of the ones that are done.
    • woken_session_id is the ses_ of the session the clock started on this card, or null — set by the wake, cleared by a reassignment or a heal. It is not client-writable.
    • reported_status is the last done/blocked status the owner was told about, or null. It is not client-writable.
  • Comment — the team’s channel on a card: { id, card_id, author, body, created_at }. Any seated session can add one.

The tools

Every session seated on a board — the owner’s, every rostered member’s, and every agent of the company on the company board — is given two built-in tools.
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. The list answers { board_id, you, cards } — the board’s brd_ id, which is how a model learns the id of a board it has never written to, and you, this agent’s name on it. The single-card form answers { card, comments }. 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, four operations, chosen by op. One flat argument object — card_id, title, body, status, assignee, blocked_by — of which each operation reads its own.
operation
{ op: "create", title, body?, status?, assignee?, blocked_by? }. Requires title; a card defaults to todo, unassigned and unblocked, and a card created with a blocked_by starts blocked. 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
{ op: "update", card_id, status, body? }. A status move, and only a status move. body is the note recorded with the move (it lands on the card’s note, never on the brief) — required, in effect, on a move to done; repeat the current status (doing → doing) to write a note without moving. It is a compare-and-set: the guards below are checked in one statement, so a move that lost a race writes nothing and comes back as a named refusal. An update with no status is refused (unsupported_update) rather than accepted and silently dropped: a card’s title and blocked_by are set when it is created, and its holder is assign’s to set.
operation
{ op: "comment", card_id, body }. A comment on a card that does not exist is a refusal, never an orphan row. The author recorded is the writing session’s ses_ id.
operation
{ op: "assign", card_id, assignee }. Hands a card that is todo or blocked to a teammate by name, or takes it back with null. The name must be a live agent’s in your organization. Assigning a todo card with no open blocker is how a teammate is started on it — see above. A card someone is doing cannot be reassigned until it is todo or blocked again.

A write can be refused, and that is normal

Members move cards blind to each other, so every write is a compare-and-set and a lost race comes back as a named tool result rather than a lost update. Nothing here throws; the model reads the word and acts on it.

How it complements delegation

A card is the hand-over; a message is a nudge. The coordinator puts the plan on the board — one card per unit of work, blocked_by for order, enough in the notes to act with no other context — and assigns; the clock starts each teammate on its card. Delegation still has its place: a coordinator with a question it needs answered inside its own turn delegates and parks. The two compose, and the coordinator page says which to reach for.

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. The PATCH is an operator’s override: a person has the card on screen and outranks the claim guards the tools enforce, which exist to stop members stepping on each other, not to stop their operator. It is the one way to retitle or re-block a card after it is created. 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 session 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; the routes are on the API reference.
Board spend — the tokens members spend reading and writing it, and the sessions the clock starts — is metered against each agent’s own budget like any other call.

Back to the team overview

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