Composing boards
How an agent composes one batch per coherent unit, anchored by relation, and why the server — not the client — decides where every card lands.
Compose cards in a 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.
{
"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" } ]
}POST /api/v1/boards/checkout-review/undoSafe 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
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.
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.
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
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.
One vocabulary, generated
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, and served live by the docs tool.
Read the docs tool first
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>.
vitrinka docs # the index
vitrinka docs composing # layout, frames, templates, questions
vitrinka docs table # one element's contractDocuments nest through 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.
Shared rendering
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
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
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.