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

# Company board

> The durable coordination surface of a team: cards and comments on one board owned by the coordinator and shared by its roster.

The [company 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. A card's `assignee` is an agent's **name**, and assigning a `todo` card with no open blocker is what [starts that agent on it](/docs/team/board#assigning-a-card-starts-the-teammate-on-it) on the next tick of the board's clock — from these routes exactly as from the tool.

## Addressing a board

A board is addressed by its `brd_` id **or** by the `agt_` id of the agent that owns it — the coordinator. 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. Every agent on the owner's current roster reads and writes this board through the tools; a roster member has no board of its own to address here unless its own `multiagent.board` is on.

<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 rather than minted — from the board and the tool call for a card an agent wrote, from the board and the `key` for a card created here with one — 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 brief — what was asked, written by whoever created the card.</ResponseField>
<ResponseField name="note" type="string | null">What the holder wrote back — what was done and what is left. A member cannot see another member's conversation, so this is the hand-off; `board_write update` writes it, and a `done` must carry one.</ResponseField>
<ResponseField name="status" type="string">One of `todo`, `doing`, `blocked`, `done`. Fixed — not configurable columns. `done` is terminal for the agents' tool; this route can still change it.</ResponseField>
<ResponseField name="assignee" type="string | null">An agent's **name**, not an id. `null` is unclaimed. A `todo` card with an assignee and no open blocker is due to be worked: the board's clock starts a session of that agent on it.</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, and on the next tick.</ResponseField>
<ResponseField name="woken_session_id" type="string | null">The session (`ses_…`) the board's clock started on this card, or `null`. Set when the assignee is woken; cleared when the card is reassigned or healed back to `todo`, so the new hand is woken in turn. Read-only: it is not accepted in any request body.</ResponseField>
<ResponseField name="reported_status" type="string | null">The last `done` or `blocked` status the board's owner was told about in a digest, or `null`. A card whose `status` differs from this is unreported and goes into the owner's next digest. Read-only.</ResponseField>
<ResponseField name="wake_count" type="integer">How many sessions the board's clock has started for this card. A card is started at most three times; the next time it would be due it is parked `blocked` for the owner instead. A schedule's standing card is the exception: the clock never starts it — its own fire does — so the count climbs with the schedule and the cap does not apply while the card is the schedule's.</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. This is the owner's board: a roster member has no board of its own, and its cards are on its coordinator's.

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.",
        "note": "Revenue and expenses reconcile; the tax line is off by 2%, chasing it.",
        "status": "doing",
        "assignee": "fact-checker",
        "blocked_by": [],
        "woken_session_id": "ses_01j9...",
        "reported_status": null,
        "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 — or, under a `key` the board has seen before, `200 OK` with the card that key already made.

<ParamField body="title" type="string" required>1–200 characters.</ParamField>
<ParamField body="body" type="string">The brief, up to 8192 characters.</ParamField>
<ParamField body="status" type="string">`todo` (default), `doing`, `blocked` or `done`. A card created with a non-empty `blocked_by` starts `blocked` unless this says otherwise.</ParamField>
<ParamField body="assignee" type="string | null">An agent's name, up to 128 characters, and it must be one a **live agent in this organization carries** — a name nobody answers to is refused (see [Refusals](#refusals)). Defaults to `null`. With an assignee and no open blocker, the card is due and the agent is started on it on the next tick.</ParamField>
<ParamField body="blocked_by" type="string[]">Up to 50 `crd_` ids this card waits on.</ParamField>
<ParamField body="key" type="string">An idempotency key, 1–128 characters, unique within the board. The card's id is derived from it, so a second `POST` under the same key answers the **existing** card with `200 OK` and writes nothing — the rest of the body is ignored on a replay. Without it every request writes its own card. This is what lets a [template](/docs/concepts/templates) seed a board on every apply without doubling its cards.</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 '{ "key": "q3-report:verify", "title": "Verify the Q3 numbers", "assignee": "fact-checker" }'
  ```
</CodeGroup>

<ResponseExample>
  ```json 201 Created theme={"system"}
  {
    "id": "crd_8k12...",
    "object": "board_card",
    "board_id": "brd_mgng...",
    "title": "Verify the Q3 numbers",
    "body": "",
    "note": null,
    "status": "todo",
    "assignee": "fact-checker",
    "blocked_by": [],
    "woken_session_id": null,
    "reported_status": null,
    "created_at": "2026-08-20T09:14:02Z",
    "updated_at": "2026-08-20T09:14:02Z"
  }
  ```

  ```json 200 OK (same key again) theme={"system"}
  {
    "id": "crd_8k12...",
    "object": "board_card",
    "board_id": "brd_mgng...",
    "title": "Verify the Q3 numbers",
    "body": "",
    "note": null,
    "status": "doing",
    "assignee": "fact-checker",
    "blocked_by": [],
    "woken_session_id": "ses_01j9...",
    "reported_status": null,
    "created_at": "2026-08-20T09:14:02Z",
    "updated_at": "2026-08-20T09:31:44Z"
  }
  ```
</ResponseExample>

The replay answers the card as it stands now — here already claimed and being worked — not as the request described it. The status code is how a seeding script tells `created` from `unchanged`.

## 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}` → `200 OK` with the updated card object. Every field of the create body but `key` is accepted and every one is optional, so a move is `{"status": "doing"}` and nothing else.

<ParamField body="title" type="string">1–200 characters.</ParamField>
<ParamField body="body" type="string">Replaces the brief, up to 8192 characters.</ParamField>
<ParamField body="note" type="string">Replaces the holder's note, up to 8192 characters — the same field a member's `board_write update` writes.</ParamField>
<ParamField body="status" type="string">`todo`, `doing`, `blocked` or `done`. No race guard: a card in `done` can be moved here. A move to `done` still needs a `note` — on the call or already on the card.</ParamField>
<ParamField body="assignee" type="string | null">Hand the card to an agent by name — one a live agent in this organization carries, or the write is refused (see [Refusals](#refusals)) — or `null` to unclaim it. **Omitting the key leaves the current holder alone; sending `null` does not.** Writing this field clears `woken_session_id`, so a `todo` card handed to someone new is due again and the new assignee is started on it on the next tick.</ParamField>
<ParamField body="blocked_by" type="string[]">Replaces the list of `crd_` ids this card waits on, up to 50.</ParamField>

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

<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 — so this route never answers a refusal, and this is the only way to retitle or re-block a card after it is created.
</Note>

## Refusals

The routes on this page fail the ordinary ways — `400 invalid_request` for a body the schema rejects, `404 not_found` for a board or card that is not there. The `PATCH` refuses exactly one move: `{ status: "done" }` with no `note` on the call and none on the card is `409 state_conflict`, its message opening with `missing_note`.

Both the `POST` and the `PATCH` also refuse an `assignee` that is not a live agent's name in the organization: `400 validation_failed`, `param: "assignee"`, its message opening with `unknown_member`. The clock joins `assignee` to a live agent, so a card handed to a name nobody carries would never be due and never worked — it would sit in `todo` reading "waiting to start" indefinitely. Sending `null` unclaims and is never refused. The check happens at the write: an agent renamed or deleted afterwards leaves the card holding a name nothing answers to, which the board shows as a stalled card.

Every other compare-and-set guard lives in the agents' `board_write`, where a lost race comes back as a named tool result rather than an error:

| Refusal            | Meaning                                                                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `already_claimed`  | The card is held by another teammate (on a claim), or someone is `doing` it (on an `assign`).                                                    |
| `already_working`  | The writer already holds a `doing` card on this board.                                                                                           |
| `blocked`          | The card's `blocked_by` names cards that are not `done`.                                                                                         |
| `already_resolved` | The card is `done`; it is not reopened or reassigned.                                                                                            |
| `missing_note`     | A move to `done` with nothing written on the card.                                                                                               |
| `unknown_member`   | `assign` named an agent that does not exist in the organization. The `POST` and `PATCH` above refuse the same name as a `400 validation_failed`. |
| `missing_assignee` | `assign` without an `assignee` (a name, or `null`).                                                                                              |
| `unknown_card`     | No such card on this board.                                                                                                                      |

They are described in full on the [board page](/docs/team/board#a-write-can-be-refused-and-that-is-normal). An operator who needs the same outcome the tool refused — reopening a `done` card, say — has the `PATCH` above.

## 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, how a card starts its assignee, and how the board composes with delegation.
</Card>
