# Composing boards (https://docs.vitrinka.ai/dev/compose)



## Compose cards in a batch [#batch]

An agent composes with one tool, `compose_board`, one batch per coherent unit — a section, a readout, a comparison — because placement and wiring are computed per batch; a single card is a batch of one. A batch holds up to sixty cards, the wires between them, and any questions it wants to leave anchored to a card. The call is all-or-nothing — on a `400` the error names the offending card by index and nothing was placed — and the last batch can be reverted with one request, which is also the only thing an agent's undo ever touches; the user's own ⌘Z never reverts agent work.

```json
{
  "board": "checkout-review",
  "layout": "row",
  "section": "Pass 3",
  "cards": [
    { "ref": "summary", "kind": "prose", "payload": { "md": "Discount applied twice on the cart step." } },
    { "ref": "table", "kind": "table", "payload": { "cols": [{ "key": "step", "label": "Step" }, { "key": "expected", "label": "Expected" }, { "key": "actual", "label": "Actual" }], "rows": [{ "step": "Cart", "expected": "-10%", "actual": "-20%" }] } }
  ],
  "edges": [ { "from": "summary", "to": "table" } ]
}
```

```http
POST /api/v1/boards/checkout-review/undo
```

## Safe retries [#retries]

Give each logical `compose_board` or `update_cards` operation a unique
`requestKey` (1–128 visible ASCII characters). HTTP callers can instead send
the same value in `Idempotency-Key`. An identical retry returns the original
receipt without creating or editing anything again, even after later edits
or a server restart. Reusing the key with different input returns `409`.
Keys belong to the authenticated principal, board and operation; a new
operation needs a new key. A template uses the expansion from its first
successful commit. Invalid batch edits roll back together.

## The server owns placement [#placement]

Nothing in a compose call is a coordinate. The agent expresses intent — a `layout` (`column`, `row`, `grid`), an `anchor` `{cardId | ref, relation}` at batch level or per card (a card's own anchor overrides the batch's and may target a `ref` in the same call), a `section` to land inside, a `journey` or `pass` to extend — and the server resolves it. `after` and `before` insert in reading order inside the anchor's container, `under` is the support row below (`after` inside a managed layout), `beside` is a peer to the right, `into` appends to a frame; the receipt reports `placedAs` and `container`, and an unsatisfiable relation is a `400 relation_unsupported` with nothing placed. Unanchored batches lay out as shelf rows. Later, `arrange` re-lays a scope with a named mode — among them `column`, `row`, `grid`, `flow`, `lanes`, `timeline`, `compare`, `journey` (a left-to-right tree driven by the board's wires) and `graph` (a top-down tree of documents driven by their `ref` wires); the server answers an unknown mode with the full list — and with `save` persists that layout onto a section so the server keeps reflowing it as cards arrive.

<Callout type="warn">
  Unknown modes and kinds are rejected with the allowed values; retired names may include a suggested replacement. Use the current canonical names in the reference when calling `compose_board`.
</Callout>

A frame's `children` array defines its reading order, independently of the
order of definitions in `cards`. Parent and order are stored independently of
measured bounds and returned as `placement: {parentId, order}`; parent `0`
means an explicit root. New frames default to a maintained two-column grid,
including empty frames. Set `payload.layout: null` for manual placement.
Use `payload.layout` on the frame to retain
its row, column or grid arrangement when content changes. The batch-level
`layout` chooses how that batch initially lands.

## Read back, edit, remove [#edit]

A receipt proves placement, not readable fit: read the board back with `scrape_board` — one digest of the cards with their section, media URLs, notes, annotations, journey chains and questions; `section` isolates one frame — and say what you did not check. Edit an AI-layer element by reference: `read_element {id}` returns an outline with its revision and JSON Pointer paths, and `edit_element {id, revision, edits}` changes only those paths; a `409` means re-read, never a blind retry. `update_cards` patches your own AI-layer cards and frames and is the one removal door. Removal is soft — restorable for 30 days: trashing your own cards is free up to 25 per call and 30% of the board; anyone else's, or a bigger batch, answers `409 needs_confirm` — repeat the same ids with the token and a reason. From a shell the same door is `vitrinka board cards rm <board> <ids> --why "…"` — [boards](/dev/cli/boards#put).

## One vocabulary, generated [#vocabulary]

The `kind` of every card is a closed vocabulary generated from the server's registry: board card kinds (`text`, `note`, `shape`, `callout`, `checklist`, `cluster`, `section`, `step`, `compare`, `doc`, `link`, `board`, `diagram`, `api`, `page`, `artifact`, `chat`) plus every element kind. The same list gates the REST door, the element door and the `compose_board` schema, so a payload that composes on one deployment composes on every deployment of the same version. It is rendered into the [elements reference](/reference/elements), and served live by the `docs` tool.

## Read the docs tool first [#docs]

The `docs` MCP tool — and `vitrinka docs [topic]` from a terminal — is this deployment's unified reference, layered for token economy: no topic returns a compact index of every kind and guide; one topic returns that page only, with the payload shape, an example and the gotchas. The board guides are `composing`, `organization`, `sections`, `uploads`, `recall`, `annotation`, `review`, `brief`, `tasks` and `project-transfer`; the procedures the skills follow are `guide:listening`, `guide:publish-capture`, `guide:brainstorm-flow` and `guide:sessions-pipeline`; `kind:<kind>` answers a generic verb's kind; a bare kind name answers with the element contract; kit component pages are `artifact:<name>`.

```bash
vitrinka docs                 # the index
vitrinka docs composing       # layout, frames, templates, questions
vitrinka docs table           # one element's contract
```

## Documents nest through refs [#refs]

A document is a card of elements, and documents nest through `ref` elements, never through links alone: `{ "kind": "ref", "payload": { "card": 812, "mode": "preview" } }` embeds, previews or links another card (in a pushed `doc.json` the same block is written flat, and may name a sibling `page` or an in-document `section` instead of a card). The server stamps the resolved target and draws hairline `ref` wires between the cards, `arrange` in `graph` mode lays a documentation tree from those wires, and the solo view's outline rail reads the neighbourhood the same wires define. `vitrinka board push` mints pages as sibling cards and rewrites `page:` references to ids in one request — [sets](/dev/cli/sets#docs).

## Shared rendering [#faces]

Elements use the same renderer on the board, in a published document and in the standalone view at `/a/<id>`. For capture or printing, `?press=1` removes inner scrolling and hover controls and loads media eagerly. `?theme=` overrides the saved theme. The capture service uses this view, and the same painter feeds the exports: `GET /api/v1/boards/{slug}/cards/{id}.pdf`, `.eml` (an email draft with inline images) and `.docx`, plus HTML from the viewer. An element tagged with an `audience` — `public`, `technical`, `internal` — appears only for those readers, and `?audience=` cuts every export the same way.

## Templates [#templates]

`get_templates` lists the built-in skeletons — QA session, iteration review, decision map, metrics dashboard, presentation deck, finding triage, UI proposal, architecture map, data story, testing-journey suite, page, importers, docs board, chat evidence — and the registry templates saved with `save_template` (a `rare`-module tool) — either a compose-shaped body with `{{slot}}` placeholders, or a live section extracted as a skeleton. `compose_board` with `template` and `params` instantiates one, and the recommended first move on a new board is to fetch the list once and start from a matching template.

## Questions and review [#questions]

A compose call can leave questions anchored to cards (`ask_board` asks one on its own). The human answers on the board — on a brainstorm board, in the brainstorm studio — and the answers stay staged until they press ↑ Send, then arrive as choice items on `wait_for_work`; `get {kind: "questions", board}` is the durable record. A server-side AI review pass is `review_judge` (a `rare`-module tool): the judge and an independent auditor produce version-pinned judgments you read with `review_judgments`, and nothing is filed on the board automatically — filing a judgment is an explicit, human-decided step. `request_review` remains only as a compatibility name. `highlight` draws a live rectangle on an annotation's shot to point at what the agent actually means.
