> ## 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 coordination surface of a team: cards and comments on a board owned by one agent.

The [team board](/docs/team/board) is the durable half of a [team](/docs/team/overview) — a coordinator's transcript scrolls away, the card that says who holds what does not. These are the routes a person, a script or a dashboard reads and writes it through; the agents themselves use the `board_read` and `board_write` tools, against the same rows.

## Addressing a board

A board is addressed by its `brd_` id **or** by the `agt_` id of the agent that owns it. That is not a convenience. A board is **minted on first use**, one per agent, and a board with no cards has never told anyone its `brd_` id — so on a brand-new team the agent id is the only reference a caller can have.

One board per agent — `unique (org_id, agent_id)` — so `POST /v1/boards` is an **ensure**, not a create that can conflict. It is also not the only way a board comes into being: the first read or write against the `agt_` spelling mints one too.

<Warning>
  **There is no delete call.** A card is never removed: a finished card is the record of what the team was asked to do, and a task that vanishes takes the answer to "what happened here" with it. Move it to `done` instead.
</Warning>

Every route here is gated by `agents:read` for reads and `agents:write` for writes: a board belongs to an agent, and the scope grammar is fixed.

## The board object

<ResponseField name="id" type="string">Unique id (`brd_…`).</ResponseField>
<ResponseField name="object" type="string">Always `board`.</ResponseField>
<ResponseField name="agent_id" type="string">The agent that owns the board (`agt_…`). One board per agent.</ResponseField>
<ResponseField name="cards" type="object">How many cards stand in each column: `{ "todo": n, "doing": n, "blocked": n, "done": n }`. All four keys are always present, so `0` and "absent" never read the same. This is a real aggregate over the board, not the length of a page — which is the one fact a page of cards cannot state, because a page is truncated and a count is not.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>

## The card object

<ResponseField name="id" type="string">Unique id (`crd_…`). Derived from the board, the writing session and the title, so a retried write collides with the row it already made rather than duplicating it.</ResponseField>
<ResponseField name="object" type="string">Always `board_card`.</ResponseField>
<ResponseField name="board_id" type="string">The board this card is on (`brd_…`).</ResponseField>
<ResponseField name="title" type="string">Short label, 1–200 characters.</ResponseField>
<ResponseField name="body" type="string">The card's notes — what was done and what is left. A member cannot see another member's conversation, so this is the hand-off.</ResponseField>
<ResponseField name="status" type="string">One of `todo`, `doing`, `blocked`, `done`. Fixed — not configurable columns.</ResponseField>
<ResponseField name="assignee" type="string | null">A roster member's **name**, not an id. `null` is unclaimed.</ResponseField>
<ResponseField name="blocked_by" type="string[]">The `crd_` ids this card waits on. A `blocked` card with an empty list is stuck on the world. When every card in the list reaches `done`, this card is promoted back to `todo` on the next read.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>
<ResponseField name="updated_at" type="string">Last modification timestamp.</ResponseField>

## The comment object

<ResponseField name="id" type="string">Unique id (`bcm_…`).</ResponseField>
<ResponseField name="object" type="string">Always `board_comment`.</ResponseField>
<ResponseField name="card_id" type="string">The card commented on (`crd_…`).</ResponseField>
<ResponseField name="author" type="string">Who wrote it, as an id **the server derived** — never a name the caller supplied. A person's comment carries their `usr_` or `key_` principal; a member's, written through `board_write`, carries the `ses_` id of the thread. The prefix is what lets a reader tell an agent's comment from a person's.</ResponseField>
<ResponseField name="body" type="string">The comment text.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>

## Ensure a board

`POST /v1/boards` → the board object. `201 Created` when this call minted the row, `200 OK` when it found one already there.

<ParamField body="agent_id" type="string" required>The agent that owns the board (`agt_…`). `404 not_found` if this organization has no such agent.</ParamField>

Calling it twice is not a conflict. There is one board per agent, so the second call answers the same board and the status code is the only difference between the two outcomes — a caller that does not care ignores it and reads the same board either way.

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/boards \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -d '{ "agent_id": "agt_9f2c..." }'
  ```
</CodeGroup>

<ResponseExample>
  ```json 201 Created theme={"system"}
  {
    "id": "brd_mgng...",
    "object": "board",
    "agent_id": "agt_9f2c...",
    "cards": { "todo": 0, "doing": 0, "blocked": 0, "done": 0 },
    "created_at": "2026-08-20T09:14:02Z"
  }
  ```
</ResponseExample>

## Get a board

`GET /v1/boards/{id}` → the board object. `{id}` is a `brd_` id or the owning `agt_` id.

This is the read that answers a board's own id, its owner and how much work stands on it. The cards routes answer cards.

## Read an agent's board

`GET /v1/agents/{id}/board` → the agent's cards, in **column order** (`todo`, `doing`, `blocked`, `done`) and then oldest first. Mints the board if the agent has none, so a team that has not started answers an empty page rather than a 404.

The response is the standard [pagination](/docs/api/pagination) envelope of card objects.

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/agents/agt_9f2c.../board \
    -H "authorization: Bearer sk_live_..."
  ```
</CodeGroup>

<ResponseExample>
  ```json 200 OK theme={"system"}
  {
    "data": [
      {
        "id": "crd_8k12...",
        "object": "board_card",
        "board_id": "brd_mgng...",
        "title": "Verify the Q3 numbers",
        "body": "Cross-check the filing against the model.",
        "status": "doing",
        "assignee": "fact-checker",
        "blocked_by": [],
        "created_at": "2026-08-20T09:14:02Z",
        "updated_at": "2026-08-20T09:31:44Z"
      }
    ],
    "has_more": false,
    "next_cursor": null
  }
  ```
</ResponseExample>

## List a board's cards

`GET /v1/boards/{id}/cards` → the same cards, addressed by board reference. `{id}` is a `brd_` id or the owning `agt_` id. Same envelope, same column order.

## Add a card

`POST /v1/boards/{id}/cards` → `201 Created` with the card object.

<ParamField body="title" type="string" required>1–200 characters.</ParamField>
<ParamField body="body" type="string">The card's notes, up to 8192 characters.</ParamField>
<ParamField body="status" type="string">`todo` (default), `doing`, `blocked` or `done`.</ParamField>
<ParamField body="assignee" type="string | null">A roster member's name, up to 128 characters. Defaults to `null`.</ParamField>
<ParamField body="blocked_by" type="string[]">Up to 50 `crd_` ids this card waits on.</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/boards/agt_9f2c.../cards \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -d '{ "title": "Verify the Q3 numbers", "assignee": "fact-checker" }'
  ```
</CodeGroup>

## Get one card

`GET /v1/boards/{id}/cards/{cid}` → the card object. A card that is not on the named board answers `404 not_found`, even if the id exists elsewhere in your organization.

## Update a card

`PATCH /v1/boards/{id}/cards/{cid}` → the updated card object. Every field of the create body is accepted and every one is optional, so a move is `{"status": "doing"}` and nothing else. `"assignee": null` unclaims the card; omitting the key leaves the current holder alone.

<Note>
  This is an **operator's override**, and it is deliberately more powerful than the agents' own `board_write`. A person has the card on screen and outranks the claim guards, which exist only to stop two members stepping on each other — and this is the only way to retitle or reassign a card, because through the tool those are set at create.
</Note>

## Read a card's comments

`GET /v1/boards/{id}/cards/{cid}/comments` → the card's comments, **oldest first**, in the standard [pagination](/docs/api/pagination) envelope. A card's comments are a conversation, so they are not reversed the way a feed is.

A `cid` that is not on the named board answers `404 not_found`, the same guard the card read sits behind.

## Comment on a card

`POST /v1/boards/{id}/cards/{cid}/comments` → `201 Created` with the comment object.

<ParamField body="body" type="string" required>The comment text, 1–8192 characters.</ParamField>

**There is no `author` field, and sending one is a `400 invalid_request`.** The server signs the comment with the principal behind your key, so nobody has to invent a name for themselves — or for anyone else. Nothing used to stop an operator signing a comment `fact-checker`.

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/boards/agt_9f2c.../cards/crd_8k12.../comments \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -d '{ "body": "Filing arrived; unblocked." }'
  ```
</CodeGroup>

<Card title="The board, in concept" icon="kanban" href="/docs/team/board">
  The four statuses, the two tools agents call, and how the board composes with delegation.
</Card>
