// DOCS

Boards

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_board returns rows: [] for every dataset you don't ask for. Always pass dataset=<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:

  1. Purpose — what is this board for?
  2. Row shape — what fields per row? What's the primary identifier?
  3. Confirm structure — the proposed schema is read back to you.
  4. Create — calls create_board with the agreed schema.
  5. Seed — appends a few starter rows so the board isn't empty.
  6. Optional skills — saved agent recipes (e.g. "score each lead").
  7. Optional dashboardset_dashboard with 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. key is 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.