// DOCS

Integrations

Every external system brains talks to. Some are read sources — they poll outside services and write pages into your brain. Some are write targets — they accept a natural-language request, return a draft, and only send after you confirm. Most are both.

Recipes can declare actions the AI agent can take on your behalf (send an email, create a calendar event, open a GitHub issue, post a Monday update). Each action is described in the recipe, materialized as a discovery page in your brain, found by the LLM via semantic search, drafted to your inbox, and only sent after you confirm — same two-phase flow as the builtin writes.

This page covers how the builtin integrations work, the two-phase write pattern that every write target follows, and how you add a new integration of your own as a codex recipe.

What's connected

Ask Claude:

"What integrations do I have?"

Claude calls list_integrations which returns one row per known integration with:

  • name — the source key (e.g. gmail, monday)
  • label — display name
  • connected — whether this user has it linked
  • supports.{fetch, act, adapter} — which surfaces accept this source
  • description — one-line summary

The source enums on fetch_from_integration / act_on_integration are filtered per-user — if a source isn't in the schema's enum, you haven't connected it. Connect from /integrations/<source>.

The builtin set

Integration Read Write Notes
Gmail OAuth + offline access. Threads → email pages, attachments parsed inline.
Google Calendar OAuth. Events → calendar_event pages with parsed start/end/location/attendees.
Google Drive OAuth. Metadata-only ingest today; body extraction on a follow-up cron.
GitHub ✓ (board-link adapter) OAuth. Connect a board to a repo, pull issues/PRs/releases on a schedule.
Monday.com ✓ (board-link adapter) OAuth. Materialize Monday items into a brains board.
Telegram Per-user Bot tokens. Text your brain from your phone.
Discord Sign-in only today; channel ingest is planned.
CoinMarketCap Top 10 First "from-scratch" declarative recipe — hourly snapshot of top 10 coins.

Plus internal infra: Anthropic (every service uses Claude for LLM calls), OpenAI (embeddings — text-embedding-3-small 1536-d for hybrid search), Groq (cheap fallback for short Telegram turns).

Reading from an integration — fetch_from_integration

When you ask "any emails from Noah about Ethera?" and brains returns nothing, the cron ingestor may not have caught up. fetch_from_integration pulls fresh data from Gmail/Calendar/Drive/etc., writes new pages into brains, and returns. After it returns, re-run your search / query / list_pages and the data will be there.

await brains.fetch_from_integration({
  source: "gmail",
  input: { query: "from:noah ethera after:2026/04/01", limit: 100 },
});
// → { ingested_count: N, page_slugs: [...] }
//   (then re-call search / query / list_pages)

For gmail and drive you can pass either an explicit input: { query } (recommended — matches the codex action shape) or a bare request string (the routing shim maps it to {query: request}). For calendar you must pass input: { start, end } — NL-to-window translation is not supported on the codex path.

Prefer fetch_from_integration over the raw mcp__claude_ai_* Google MCPs because it persists into your brain — the same data is queryable from the web app and every future Claude session.

Writing to an integration — act_on_integration (two-phase)

Every write — sending an email, creating a calendar event, creating a Drive doc — is two-phase: draft, then confirm. The tools never reach the external service on their own.

// 0. Resolve the install_id from the user's gmail-inbox install
//    (via brains.query type=integration_action — the slug encodes it).

// 1. Draft.
const draft = await brains.act_on_integration({
  install_id: gmailInstallId,
  action_name: "send_email",
  input: {
    to: ["noah@a16z.com"],
    subject: "Re: next week",
    body: "We're in. Let me know what day works.",
    thread_id: priorThreadId, // optional, for proper threading
  },
});
// draft = { kind: "draft", draft_id, action, preview, payload, expires_at }

// 2a. Confirm — sends to Gmail.
await brains.confirm_action({ draft_id: draft.draft_id });
// → { status: "confirmed", result: { message_id: "<id>" } }

// 2b. Discard — no external side effect.
await brains.discard_action({ draft_id: draft.draft_id });

Always show the preview to the user before confirming. Drafts expire after one hour. The draft lives in your brain so it's auditable and resumable from /inbox.

Optional edits at confirm time

confirm_action accepts an edits object that patches whitelisted fields right before sending:

Action Editable fields
email to, cc, bcc, subject, body
event summary, description, location, start, end, attendees, send_updates
file name, content
await brains.confirm_action({
  draft_id: draft.draft_id,
  edits: { subject: "Re: Q3 — confirmed", cc: ["legal@..."] },
});

Return shapes from act_on_integration

  • { kind: "draft", draft_id, action, preview, payload, expires_at } — happy path. Show preview, wait for confirmation.
  • { kind: "clarification", question } — ambiguous request (e.g. two "Omer"s in your mailbox). Ask the user back, then re-call with a tighter request.
  • { kind: "noop", reason } — no write was appropriate. Tell the user why.

Batching drafts in one turn

If you're producing N drafts in one turn (e.g. "send a follow-up to everyone I met this week"), mint one UUID per turn and pass it as batch_id on every draft. /inbox groups them together so the user approves them as a set.

How board-link integrations work

GitHub and Monday are different — they don't draft and confirm. They're board-link adapters: a board points at the external resource, and a recipe materializes data on a schedule.

const link = await brains.create_board_link({
  board_id,
  kind: "github",
  ref: { owner: "ssvlabs", repo: "brains" },
  display_name: "brains repo PRs",
});

await brains.create_dataset_recipe({
  link_id: link.id,
  dataset_name: "prs",
  adapter_op: "github.list_open_prs",
  refresh_cron: "0 * * * *",
  dedupe_key_field: "id",
  dedupe_target_field: "pr_id",
});

Adapters live in apps/web/src/lib/adapters/ and are pluggable. Each adapter exports a set of op handlers (e.g. github.list_open_prs, monday.list_items) callable from the recipe runtime.

Building your own integration

There are two ways to add a new integration: declarative recipes (YAML-ish JSON, no code) or sandboxed recipes (TypeScript inside a Deno sandbox). Always try declarative first.

Declarative recipes — the CoinMarketCap pattern

A declarative integration is a single object that says: trigger on this cron, GET this URL with these headers, map each response item into a brains page. The runtime handles auth, retries, rate-limiting, idempotency, and metering. No code.

The minimum shape:

add({
  slug: "coinmarketcap-top10",
  name: "CoinMarketCap Top 10",
  description: "Hourly snapshot of the top 10 cryptocurrencies.",
  publisher: "<your-user-id>",
  auth: { type: "api_key", header: "X-CMC_PRO_API_KEY", secret_name: "CMC_API_KEY" },
  trigger: { kind: "cron", cron: "0 * * * *" },
  endpoints: [{
    op: "snapshot",
    method: "GET",
    url: "https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?limit=10",
    headers: { "X-CMC_PRO_API_KEY": "{{publisher.CMC_API_KEY}}" },
  }],
  mapping: {
    iterate: "$.data[*]",                  // JSONPath over response
    page_type: "crypto_coin_snapshot",
    source_ref_prefix: "cmc:",
    source_ref: "{{symbol}}@{{last_updated}}",   // idempotency key
    title: "{{name}} ({{symbol}}) — ${{quote.USD.price}}",
    frontmatter: {
      symbol: "{{symbol}}",
      price_usd: "{{quote.USD.price}}",
      market_cap_usd: "{{quote.USD.market_cap}}",
      last_updated: "{{last_updated}}",
    },
  },
});

The whole integration is one object. The source_ref field is the idempotency key — same source_ref → no duplicate page write. Pick something that's stable per snapshot (an upstream ID + timestamp works well).

A few quirks to know up front:

  1. The template engine does not support Python-style format specs ({value:.2f}, {value:,.0f}). Strings come through as String(value) — full precision, no thousands separator. Format in the consumer (dashboard, mini-site), not in the recipe.
  2. There's no {{run_iso_hour}} built-in. Derive idempotency from the response's own data (e.g. an upstream last_updated timestamp), not from run metadata.
  3. auth.type=api_key is metadata only — the runner doesn't auto-inject the header. You must reference the secret explicitly in endpoints[].headers via {{publisher.SECRET_NAME}}. The auth block exists so the installer UI knows the recipe needs that secret in the publisher vault.
  4. Frontmatter values are strings — numeric fields become stringified numbers. Downstream consumers parse as needed.

Sandboxed recipes — when declarative isn't enough

If your integration needs OAuth, pagination, conditional fetches, cursor management, or any non-trivial logic, drop into a sandboxed recipe — same Deno sandbox as a user automation, just authored as a recipe so others can install it.

The shape mirrors a normal automation: a main.ts, a tool_grants list, http_fetch hosts, and the same {{secret_name}} substitution for credentials. The difference is that a recipe is installable — other users can add it to their brain with one click.

The five existing Google/Monday integrations are all sandboxed recipes (they predate the declarative path). CoinMarketCap is the first declarative one built from scratch.

Lifecycle

Every integration recipe has a publisher (you) and N installers (other users). When you publish a new version:

  • Existing installers see "Update available" in the codex.
  • Installs can be standalone or bundled (installed via a parent bundle). Bundled installs upgrade with the bundle; standalone installs upgrade individually.
  • Per-install state (cursors, dedupe sets, OAuth tokens) is preserved across upgrades.

Anthropic, OpenAI, Groq

These aren't integrations you connect — they're infrastructure brains uses internally:

  • Anthropic — every service that runs an LLM call (the web Agent SDK, act_on_integration, automation_llm_complete, board skills, board form intake, chat summarization, Telegram). Cost capture writes an llm_usage row per call with a caller tag for attribution.
  • OpenAItext-embedding-3-small (1536-d) for the query hybrid search and routing scope embeddings. The same model is used everywhere so similarity scores are comparable.
  • Groq — optional cheap fallback for short Telegram turns. If GROQ_API_KEY isn't set, every Telegram turn goes to Anthropic.

You don't connect these per-user. They're served by the platform's API keys.

API reference

Every integration primitive is an MCP tool. The schemas below are the source of truth — what the web UI and act_on_integration / fetch_from_integration flows call internally is exactly what you call from your own LLM session.

Discovery

list_integrations

Discover what's available for the current user and what's actually connected. Use before suggesting fetch_from_integration / act_on_integration so you don't propose a source the user hasn't linked.

Parameter Type Required Notes
include_unconnected boolean no Default true. Pass false to only see connected integrations.

Returns{ integrations: [{ name, label, oauth_provider, supports: { fetch, act, adapter }, connected, description }, …] }.

list_starter_recipes

Browse the codex — prebuilt boards, automations, workflows, and integrations ready to instantiate into a brain.

Parameter Type Required Notes
kind enum: board | automation | workflow | bundle no
category string no
tag string no Single tag filter.
search string no FTS over name + description.
limit int 1–100 no Default 50.

Returns{ recipes: [{ id, slug, name, kind, category, tags, version, description_excerpt, preview_image_url, author, created_at, updated_at }, …] }.

get_starter_recipe

Fetch one recipe in full, including the template payload that apply_starter_recipe will instantiate.

Parameter Type Required Notes
slug string one of Kebab-case slug.
id uuid one of Recipe UUID.

Returns{ recipe: {…full recipe object…} }.

Reading from an integration

fetch_from_integration

Pull fresh data from a connected integration on demand. Results are persisted as pages in the target brain so subsequent search / query / list_pages find them. For calendar, the fetch is exhaustive over the requested window across all calendars; for gmail / drive, up to 250 items per call.

Parameter Type Required Notes
source enum: gmail | calendar | drive yes
request string (1–1000) yes Free-form NL query (e.g. "events from 2026-06-01 through 2026-06-07").
brain_id uuid no Defaults to user's default brain.

Returns{ source, request, brain_id, ingested_count, page_slugs, errors?, translation_log, verify_mode?, note? }.

Two-phase writes

act_on_integration

Draft a write from a natural-language request. Never sends — returns a draft_id plus a human-readable preview. Hand the preview to the user; only confirm_action actually hits the external service.

Parameter Type Required Notes
source enum: gmail | calendar | drive yes
request string (1–1000) yes NL description of the action.
attachments array (≤5) no [{ filename, content_base64, mime_type? }, …] — 10 MB each max.
attach_from_page string (≤500) no Gmail only: extract attachments from an existing email page slug.
notify array of inbox | telegram no Where to push the approval prompt. Default ["inbox"].
batch_id uuid no Group multiple drafts produced in one turn so /inbox clusters them.

Returns — one of:

  • { kind: "draft", draft_id, preview, payload, expires_at, … } (happy path; show preview).
  • { kind: "clarification", question } (ambiguous; ask the user back, then re-call).
  • { kind: "noop", reason } (no write was appropriate).

confirm_action

Execute a draft. One-shot per draft. Optional edits patch whitelisted fields immediately before sending:

Action Whitelisted edit fields
email to, cc, bcc, subject, body
event summary, description, location, start, end, attendees, send_updates
file name, content
Parameter Type Required Notes
draft_id uuid yes
edits object no See whitelist above.

Returns{ status: "confirmed", result: { message_id? \| event_id? \| file_id? \| modified_count? } } or { status: "failed", error }.

discard_action

Drop a draft without sending. No external side effects.

Parameter Type Required Notes
draft_id uuid yes

Returns{ ok: true, draft_id, status: "discarded" }.

confirm_actions (batch)

Parameter Type Required Notes
draft_ids uuid[] (1–50) yes Executed in order. Failures don't stop later drafts.

Returns{ results: [{ draft_id, status: "confirmed" \| "failed", source?, action?, result?, error? }, …] }.

discard_actions (batch)

Parameter Type Required Notes
draft_ids uuid[] (1–100) yes

Returns{ results: [{ draft_id, status: "discarded" \| "failed", error? }, …] }.

list_pending_actions

List outstanding drafts (status=pending, not expired). Does not execute anything.

Parameter Type Required Notes
limit int 1–100 no Default 50.
source enum no Filter by source.

Returns{ count, drafts: [{ draft_id, source, action, preview, created_at, expires_at, notify_targets }, …] }.

ack_inbox_action

Record a user decision on an inbox prompt from the SessionStart hook (one-shot per (action, user)). Most callers don't need this — the web UI files it automatically when the user clicks Approve/Decline.

Parameter Type Required Notes
id uuid yes Action UUID from the HTML marker.
decision enum: approved | declined | auto yes

Returns{ ok: true, id, decision, recorded }.

Board-link adapters

create_board_link

Bind a board to an external resource via an adapter (http_json, ics, github, monday, …). ref is adapter-specific. For private resources, pass integration_provider so brains uses the caller's OAuth credentials.

Parameter Type Required Notes
board_id uuid yes
kind string (1–100) yes Adapter kind.
ref object yes Adapter-specific (URL, { owner, name }, …).
display_name string (1–200) yes
integration_provider string no OAuth provider name.
integration_user_id uuid no Whose credentials to use. Defaults to caller.

Returns — the created link row.

create_dataset_recipe

Schedule (or define for manual refresh) a materialization from a link into a board dataset. The recipe replaces data.datasets.<dataset_name>.rows on each refresh — mark such datasets read-only by convention.

Parameter Type Required Notes
link_id uuid yes
dataset_name string (1–100) yes
adapter_op string (1–100) yes e.g. github.list_open_prs.
params object no Adapter args.
refresh_cron string no Omit / null = manual refresh only.

Returns — the created recipe row.

Installing and upgrading recipes

apply_starter_recipe

Instantiate a starter recipe into a brain. Dispatches to the right creator (create_board, save_automation_draft, create_workflow). Automations are always created paused.

Parameter Type Required Notes
slug string one of Recipe slug.
id uuid one of Recipe UUID.
brain_id uuid no Default brain if omitted.
overrides object no { board_name?, board_description?, title?, charter?, roster? }.

Missing dependency recipes are ALWAYS cascade-installed before the target — there is no opt-out flag.

Returns{ recipe, created, dependencies_used?, recovered?, cascade_installed? }.

check_recipe_dependencies

Pre-flight a recipe install — does the caller have every prerequisite already?

Parameter Type Required Notes
slug string one of
id uuid one of
brain_id uuid no

Returns{ recipe, declared, satisfied, missing: [{ slug, min_version, reason?, available: { has_recipe, latest_version } }, …], chain }.

check_recipe_upgrade

Check whether an instantiated entity has a newer recipe version available and whether upgrading would clobber user changes. Row content does not count as a user change.

Parameter Type Required Notes
entity_id uuid yes
entity_kind enum: board | automation | workflow yes

Returns{ installed, latest, has_upgrade, has_user_changes, change_summary }.

upgrade_recipe

Rebuild an instantiated board/automation/workflow from the latest recipe version. Preserves row content, automation state, workflow roster. Replaces datasets/skills/dashboard (boards), source / cron / grants / caps (automations), charter / KPIs / deadlines (workflows).

Parameter Type Required Notes
entity_id uuid yes
entity_kind enum: board | automation | workflow yes
force boolean no Default false. Discard user changes and rebuild.

Returns{ entity_id, entity_kind, upgraded_from_version, upgraded_to_version, … }.

uninstall_recipe

Uninstall a recipe-spawned entity (or every entity stamped with a slug, for bundles) with a 7-day recovery window. Sets uninstalled_at rather than hard-deleting.

Parameter Type Required Notes
entity_id uuid one of Single entity to uninstall.
slug string one of Uninstall all instances of this recipe.
brain_id uuid with slug Required when using slug.
cascade boolean no Default true for bundles.

Returns{ uninstalled: [{ kind, id, name, recoverable_until }, …], cascade_count }.

list_uninstalled

List entities currently inside their 7-day recovery window.

Parameter Type Required Notes
brain_id uuid no Optional brain scope.
limit int 1–500 no Default 100.

Returns[{ kind, id, name, source_recipe_slug, uninstalled_at, expires_at }, …].

recover_recipe

Recover an entity within its 7-day window. Flips uninstalled_at back to NULL; rows / source / provenance all intact.

Parameter Type Required Notes
entity_id uuid yes

Returns{ kind, id, name, source_recipe_slug, recoverable_until, … }.