> ## 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](https://vetta.sh/docs/team/board.md) is the durable half of a [team](https://vetta.sh/docs/team/overview.md) — 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](https://vetta.sh/docs/team/board.md#assigning-a-card-starts-the-teammate-on-it). Through these routes it starts in the same request, and the response carries `woken_session_id`; through the tool — a `create`, an `assign`, or a move that frees the card — as soon as the write lands.

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

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

**id** `string`

Unique id (`brd_…`).

**object** `string`

Always `board`.

**agent\_id** `string`

The agent that owns the board (`agt_…`). One board per agent.

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

**created\_at** `string`

Creation timestamp.

## The card object

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

**object** `string`

Always `board_card`.

**board\_id** `string`

The board this card is on (`brd_…`).

**title** `string`

Short label, 1–200 characters.

**body** `string`

The brief — what was asked, written by whoever created the card.

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

**status** `string`

One of `todo`, `doing`, `blocked`, `done`. Fixed — not configurable columns. `done` is terminal for the agents' tool; this route can still change it.

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

**blocked\_by** `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, on the next tick, and at once when a teammate's `board_write` finishes the last one on the same board.

**woken\_session\_id** `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.

**reported\_status** `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.

**wake\_count** `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.

**created\_at** `string`

Creation timestamp.

**updated\_at** `string`

Last modification timestamp.

## The comment object

**id** `string`

Unique id (`bcm_…`).

**object** `string`

Always `board_comment`.

**card\_id** `string`

The card commented on (`crd_…`).

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

**body** `string`

The comment text.

**created\_at** `string`

Creation timestamp.

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

**agent\_id** `string` *required*

The agent that owns the board (`agt_…`). `404 not_found` if this organization has no such agent.

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.

```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..." }'
```

```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"
}
```

## 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](https://vetta.sh/docs/api/pagination.md) envelope of card objects.

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

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

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

**title** `string` *required*

1–200 characters.

**body** `string`

The brief, up to 20,000 characters.

**status** `string`

`todo` (default), `doing`, `blocked` or `done`. A card created with a non-empty `blocked_by` starts `blocked` unless this says otherwise.

**assignee** `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 in this request — unless it is busy with another card on this board, when the card waits its turn. A create with a `key` is a seed, as a template's are, and is left to the board's clock.

**blocked\_by** `string[]`

Up to 50 `crd_` ids this card waits on.

**key** `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](https://vetta.sh/docs/concepts/templates.md) seed a board on every apply without doubling its cards.

```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" }'
```

```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"
}
```

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.

**title** `string`

1–200 characters.

**body** `string`

Replaces the brief, up to 20,000 characters.

**note** `string`

Replaces the holder's note, up to 20,000 characters — the same field a member's `board_write update` writes.

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

**assignee** `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 in this request, or after their current card if they are busy.

**blocked\_by** `string[]`

Replaces the list of `crd_` ids this card waits on, up to 50.

```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" }'
```

> [!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.

## 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](https://vetta.sh/docs/team/board.md#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](https://vetta.sh/docs/api/pagination.md) 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.

**body** `string` *required*

The comment text, 1–20,000 characters.

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

```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." }'
```

> [card] [The board, in concept](https://vetta.sh/docs/team/board.md)
>
> The four statuses, the two tools agents call, how a card starts its assignee, and how the board composes with delegation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.