> ## Documentation Index
> Fetch the complete documentation index at: https://vetta.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Team board

> The durable half of a team: a shared board every member reads and writes, with four fixed statuses and cards that outlive the run.

The board is one of the [two ways a team coordinates](/docs/team/overview#two-ways-a-team-coordinates) — the durable, broadcast one. Where [delegation](/docs/team/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

```json Agent config theme={"system"}
{
  "multiagent": {
    "type": "coordinator",
    "agents": [{ "type": "agent", "id": "agt_9f2c…", "version": 3 }],
    "board": true
  }
}
```

`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`](/docs/cli/team). When it is false the two board tools are simply not constructed, and the team has delegation only.

<Note>
  **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`](/docs/api/board#ensure-a-board), `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.
</Note>

## Four statuses, fixed

```
   todo              doing             blocked            done
   ┌───────┐         ┌───────┐         ┌───────┐          ┌───────┐
   │ card  │         │ card  │◀─ owner │ card  │          │ card  │
   │ card  │         └───────┘         └───────┘          │ card  │
   └───────┘                            blocked_by ──┐    └───────┘
                                                     └──▶ crd_…
```

`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.

<Note>
  **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](/docs/concepts/harness-capabilities#platform-injected-tools).
</Note>

### `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.

| Argument  | Effect                                                                              |
| --------- | ----------------------------------------------------------------------------------- |
| `status`  | Only cards in that column.                                                          |
| `owner`   | Only cards assigned to that teammate.                                               |
| `card_id` | **One** card, in full — its notes and up to 50 comments. Ignores the other filters. |
| `limit`   | Page size; 50 by default, 100 at most.                                              |

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`.

<ResponseField name="op: &#x22;create&#x22;" type="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.
</ResponseField>

<ResponseField name="op: &#x22;update&#x22;" type="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.
</ResponseField>

<ResponseField name="op: &#x22;comment&#x22;" type="operation">
  Requires `card_id` and `body`. A comment on a card that does not exist is a refusal, never an orphan row.
</ResponseField>

### 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.

| Refusal                          | What happened                                                              |
| -------------------------------- | -------------------------------------------------------------------------- |
| `already_claimed`                | The card is another teammate's. The message names the holder.              |
| `already_working`                | You are already holding a card; finish it before claiming another.         |
| `already_resolved`               | The card is `done`. Make a new card rather than reopening it.              |
| `blocked`                        | The card waits on cards that are not finished yet. The message names them. |
| `unsupported_update`             | The update tried to change something other than status.                    |
| `unknown_card`                   | No such card on this board.                                                |
| `missing_title` / `missing_card` | The operation needs a field it was not given.                              |
| `no_board`                       | This session has no team board.                                            |

<Note>
  **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.
</Note>

## How it complements delegation

|            | [Delegation](/docs/team/delegation) | Team board                                          |
| ---------- | ------------------------------ | --------------------------------------------------- |
| Shape      | Point-to-point hand-off        | Shared, many-to-many                                |
| Visibility | Only the two sessions involved | The whole team                                      |
| Lifetime   | The delegated session          | Outlives every session that touched it              |
| Best for   | "Do this specific thing now"   | Tracking goals, progress, blockers, and async notes |

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.

| Route                                       | What it does                                                                                          |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `POST /v1/boards`                           | Ensure the agent's board. `{"agent_id": "agt_…"}`; `201` when it minted one, `200` when it found one. |
| `GET /v1/boards/{id}`                       | One board: its id, its owner, and how many cards stand in each column.                                |
| `GET /v1/agents/{id}/board`                 | The agent's board, as its cards, in column order. Creates the board on first read.                    |
| `GET /v1/boards/{id}/cards`                 | The same cards, addressed by board reference.                                                         |
| `POST /v1/boards/{id}/cards`                | Add a card. Only `title` is required.                                                                 |
| `GET /v1/boards/{id}/cards/{cid}`           | One card.                                                                                             |
| `PATCH /v1/boards/{id}/cards/{cid}`         | Move, retitle, assign or block it. `assignee: null` unclaims.                                         |
| `GET /v1/boards/{id}/cards/{cid}/comments`  | A card's comments, oldest first.                                                                      |
| `POST /v1/boards/{id}/cards/{cid}/comments` | Comment on a card. `{"body": "…"}` — the author is derived, never supplied.                           |

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`](/docs/cli/board); from the client, [`client.board`](/docs/sdk/board).

```bash theme={"system"}
vetta board show research-lead --status blocked
vetta board add research-lead --title "Verify the Q3 numbers" --assignee fact-checker
```

Board spend — the tokens members spend reading and writing it — is metered against the [budget](/docs/team/context-and-budgets) like any other call.

<Card title="Back to the team overview" icon="network" href="/docs/team/overview">
  The two mechanisms, the coordinator model, rosters, and how context and budget flow.
</Card>
