A board is the user-facing "spreadsheet, but actually a brain." Each board lives inside a brain, holds typed rows in named datasets, and is the substrate for almost everything else you build: automations write to it, mini-sites render it, intake forms append to it, dashboards visualize it.
What a board actually is
brain
└── board
├── data { datasets: { "<name>": { rows: [...], schema: {} } } }
├── schema { version: 2, datasets: [ { name, description, fields[] } ] }
├── skills named agent recipes scoped to this board
├── forms chat intake forms that append rows
├── links pointers to external HTTP / GitHub / Monday / ICS data
│ └── recipes link + cron → materialize into a dataset
├── files binary attachments
└── dashboard one sandboxed HTML iframe per board, owner-only edits
Rows and datasets
Boards hold rows grouped into named datasets. The default dataset
is named default; tools that don't take a dataset argument target
it. Most boards only ever need default. Boards that model
relationships (a CRM with companies and leads and meetings, a
research tracker with findings and experiments) use multiple
datasets to keep parallel row shapes side-by-side without forcing
separate boards.
Each row carries a stable row_id generated when the row is appended.
All update/delete operations address rows by row_id, not by
position, so concurrent appends are safe.
get_boardreturnsrows: []for every dataset you don't ask for. Always passdataset=<name>explicitly when you need rows from a non-default dataset.
The "board brain" idea
Each board is its own little shared brain. The schema, the rows, the saved skills, the dashboard — all of it is memory that you, your collaborators, and any agent you grant access to read and write together. When an automation appends rows, when a board form intake fills one in, when a skill enriches an existing row, they're all writing to the same shared memory.
Creating a board — the right way
Ask Claude:
"Create a board to track inbound investor meetings."
Claude runs create_board_flow which walks you through:
- Purpose — what is this board for?
- Row shape — what fields per row? What's the primary identifier?
- Confirm structure — the proposed schema is read back to you.
- Create — calls
create_boardwith the agreed schema. - Seed — appends a few starter rows so the board isn't empty.
- Optional skills — saved agent recipes (e.g. "score each lead").
- Optional dashboard —
set_dashboardwith a starter HTML template.
The playbook exists so the board you end up with has skills, a
dashboard, and a schema that fits what you wanted to track. Don't
call create_board directly unless you're reproducing an existing
board verbatim — hand-rolled boards typically forget the dashboard
and skills.
Reading and writing from code
All boards are accessible via MCP tools. Inside an automation:
import { brains } from "https://brains/sdks/automation-sdk-v1.ts";
const board = await brains.get_board({ name: "Inbound investors" });
const existing = new Set(
board.data.datasets.default.rows.map((r) => r.email_id),
);
const recent = await brains.search({ q: "investor", type: "email", limit: 50 });
const next = recent.filter((p) => !existing.has(p.id));
if (next.length) {
await brains.append_board_rows({
board_id: board.id,
rows: next.map((p) => ({
email_id: p.id,
subject: p.title,
from: p.frontmatter?.from,
received_at: p.frontmatter?.internalDate,
})),
});
}
The same tools — get_board, append_board_rows, update_board_row,
delete_board_row, restore_board_row — work from the web Agent SDK,
from create_board_flow, from custom MCP clients, and from any other
LLM you connect over MCP. There is no separate "agent API" — humans
and agents use the same tools.
Editing the column schema
To change column definitions without re-sending every row, use
update_board_schema:
add_columns— append new column objects ({key, label?, field_type?, …}).remove_columns— drop columns by key. Row values under that key are kept; re-adding the key surfaces them again (non-destructive).update_columns— shallow-merge a patch over an existing column to change its label, type, or options.keyis immutable.
Use this instead of update_board (which replaces the whole data
blob) — it's faster on large boards and provably leaves rows
untouched.
Skills — saved agent recipes
A board skill is a named, parameterized prompt that operates on the board's rows. Skills give you a one-click button for the repetitive thing you keep prompting Claude to do.
await brains.create_board_skill({
board_id,
name: "score_lead",
prompt: `
You are scoring a single inbound investor row.
Row: {{row}}
Score 1–5 on: fit, stage, momentum.
Return JSON: { fit: 1-5, stage: 1-5, momentum: 1-5, notes: "" }
`,
inputs_schema: { type: "object", properties: { row_id: { type: "string" } } },
allowed_tools: ["get_board", "update_board_row"],
});
// Later, run it:
await brains.run_board_skill({
board_id,
name: "score_lead",
inputs: { row_id: "ROW-123" },
});
allowed_tools is the key: it restricts what the skill agent can
call, even though the user running the skill could call anything.
Skills always run with the minimum surface needed.
For expensive multi-turn skills, prefer authoring an automation that calls the skill on a cron — the inline runner is tuned for fast single turns.
Forms — chat intake
A board form is a chat-based intake that appends a row when complete. Useful for collecting feedback, scoping calls, or structured data from users who shouldn't see the board.
await brains.create_board_form({
board_id,
dataset: "responses",
fields: [
{ key: "name", question: "What's your name?", required: true },
{ key: "company", question: "Where do you work?", required: true },
{ key: "ask", question: "What can we help with?", required: true },
],
context:
"You're collecting product feedback for the Q2 launch — be friendly and probing.",
intro: "Thanks for sharing your perspective on the launch.",
allow_anonymous: true,
});
The respondent visits /f/<form_id>, the agent chats with them
incrementally, and the moment every required field is filled the
agent appends a row to the target dataset. Full chat transcripts are
preserved on each response so the board owner can audit how the data
was collected — read them with list_board_form_responses.
Links and recipes — external data sources
A board link is a pointer to an external resource. A dataset recipe glues a link to a dataset on a schedule.
// 1. A link tells brains where the data lives.
const link = await brains.create_board_link({
board_id,
kind: "github", // or http_json, ics, monday, …
ref: { owner: "ssvlabs", repo: "brains" },
display_name: "brains repo",
});
// 2. A recipe says "every hour, hit this op, write rows into this dataset."
await brains.create_dataset_recipe({
link_id: link.id,
dataset_name: "prs",
adapter_op: "github.list_open_prs",
params: { state: "open" },
refresh_cron: "0 * * * *",
dedupe_key_field: "id", // upstream id field
dedupe_target_field: "pr_id", // local row field
});
dedupe_key_field + dedupe_target_field make refreshes
idempotent: the recipe skips rows whose pr_id already exists on
the board. This is non-optional in practice — without it, every hour
adds duplicate rows.
Built-in adapters: http_json, ics, github, monday. Adding a
new one is a single file that exports op handlers.
Dashboards — a sandboxed iframe per board
Each board has one dashboard, visible to every brain member,
editable only by the board's creator. The dashboard is an HTML iframe
that runs sandboxed at /d/<board_id> with no outbound network access.
Writes from the dashboard happen via postMessage: the iframe sends
a message to the parent window, which proxies it to the row-CRUD API
on behalf of the signed-in user. The board's permissions still apply
— the bridge only executes operations the current user is authorized
to perform.
Supported message types:
type |
Fields | Effect |
|---|---|---|
board.row.append |
dataset?, row |
Append a new row (preferred for forms) |
board.row.insert |
dataset?, row |
Alias for append |
board.row.update |
dataset?, row_id, patch |
Shallow-merge patch onto existing row |
board.row.delete |
dataset?, row_id |
Soft-delete a row |
board.row.read |
dataset?, row_id |
Read a single row |
All messages accept an optional reqId string echoed back in the
.result reply so you can match async responses.
Form-in-dashboard pattern (intake / lead capture)
<form id="f">
<input name="name" placeholder="Name" required />
<input name="email" placeholder="Email" required />
<button type="submit">Submit</button>
</form>
<div id="status"></div>
<script>
document.getElementById('f').addEventListener('submit', async (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.target));
const reqId = Math.random().toString(36).slice(2);
// Append a new row to the board's "leads" dataset.
window.parent.postMessage(
{ type: 'board.row.append', reqId, dataset: 'leads', row: data },
'*'
);
// Wait for the result from the parent bridge.
await new Promise((resolve) => {
window.addEventListener('message', function handler(ev) {
if (ev.data?.type === 'board.row.append.result' && ev.data.reqId === reqId) {
window.removeEventListener('message', handler);
document.getElementById('status').textContent =
ev.data.ok ? 'Saved' : 'Error: ' + ev.data.error;
resolve();
}
});
});
});
</script>
The bridge returns
{ type: "board.row.append.result", reqId, ok, row_id, row, dataset }
on success, or { ok: false, error } on failure. The dataset
parameter defaults to "default" if omitted.
Dashboard HTML is trusted content. The creator can author arbitrary JS that reads the board's rows and writes them back. Don't paste dashboard templates from untrusted sources.
When you edit a dashboard for a Codex-installed board, edit the recipe that installed it, not the dashboard directly — your changes get overwritten on the next recipe upgrade.
Files
board_files holds binary attachments per board. The web UI handles
drag-and-drop upload; storage is encrypted at rest. Use
upload_board_file, list_board_files, get_board_file_url,
delete_board_file.
Soft delete
Rows are soft-deleted by delete_board_row and can be restored with
restore_board_row. Boards themselves have a 30-day recovery window.
Sharing
share_board / unshare_board invite users to the brain containing
the board — in the single-owner brain model, a user who can see the
brain can see all its boards. The sharing surface exists for cases
where you want to onboard a collaborator with a single tool call from
inside a playbook or automation.
Tool surface
| Tool | Purpose |
|---|---|
create_board_flow |
Guided multi-step authoring. Use this. |
create_board |
Raw create — only for reproducing existing boards. |
get_board |
Read board + selected datasets. Always pass dataset. |
list_boards |
List all boards in a brain. |
append_board_rows |
Append rows. |
update_board_row |
Patch one row by row_id. |
delete_board_row / restore_board_row |
Soft-delete / restore one row. |
update_board_schema |
Add / remove / update columns without touching rows. |
create_board_skill / run_board_skill / list_board_skills / get_board_skill / update_board_skill / delete_board_skill |
Skills. |
create_board_form / update_board_form / list_board_forms / list_board_form_responses / delete_board_form |
Forms. |
create_board_link / list_board_links / delete_board_link |
Links. |
create_dataset_recipe / refresh_dataset_recipe |
Recipes. |
upload_board_file / list_board_files / get_board_file_url / delete_board_file |
Files. |
set_dashboard / get_dashboard |
Dashboards. |
share_board / unshare_board / list_board_members |
Sharing. |