Subagents

Scripts that read and write your workspace on a trigger.

What is a subagent?

A subagent is a small JavaScript script that lives in your workspace. It runs server-side in a sandbox, never in the browser. When a trigger fires, the script gets a fresh execution environment, receives any event payload, and talks back to Brain through the s16 API.

Through that API, a subagent can read and write database rows, call an LLM, make outbound HTTP requests, and store state between runs. Everything a subagent does is recorded on a run row: outputs, logs, tokens used, and the reason it stopped.

The sandbox

Subagent scripts run inside a node:worker_threads worker with a vm.createContext isolate. The sandbox is intentionally narrow:

No raw network

Outbound calls go through s16.http(). No fetch, no http module.

No timers

setTimeout and setInterval are removed so a runaway loop cannot stall the host.

Memory cap

V8 enforces a heap limit (default 512 MB, tunable with AGENT_MEMORY_LIMIT_MB).

Lower CPU priority

os.setPriority demotes the worker so subagents never starve the API process.

Concurrency is bounded by a shared WorkerPool (sized from cores and AGENT_CPU_PCT_PER_AGENT). Anything that overruns its limits is killed with an AbortReason of cancelled, memory, or timeout.

How do agent triggers work?

A trigger decides when a subagent runs. Each subagent can have any number of triggers attached.

Manual
manual

Fires when you click Run, or when an MCP tool calls s16_run_subagent.

Use it for: Ad-hoc runs, debugging, one-shot scripts.

Event
event

Fires on workspace events: row.created, row.updated, row.deleted, cell.changed.

Use it for: React when a row appears or a specific column changes.

Cron
cron

Fires on a schedule using standard cron syntax.

Use it for: Daily digests, hourly sync jobs, weekly reports.

Webhook
webhook

Fires on HTTP POST to a unique URL. Sync mode returns the subagent output, async mode replies 200 immediately.

Use it for: External services calling into your workspace.

Gmail
gmail

Fires when an email arrives at a connected Gmail account.

Use it for: Inbox triage, ticket creation, lead capture.

Subagent change
agent_change

Fires when another subagent updates a row, so subagents can chain.

Use it for: Multi-step pipelines where one subagent triggers the next.

What can the s16 API do?

The s16 object lets a script read and write everything in your workspace: databases, pages, docs, files, and KV state. Inside a script it is a Proxy that round-trips every call back to the host. These are the surfaces you reach for most often:

s16.db.queryDatabase(databaseId, filters)

Read rows from a database with optional filters.

s16.db.createPage(databaseId, properties)

Insert a new row.

s16.db.updatePage(pageId, properties)

Patch properties on an existing row.

s16.db.deletePage(pageId)

Delete a row.

s16.ai({ model, messages })

Call an LLM through the workspace gateway.

s16.http(url, options)

Make outbound HTTP calls (the only way to reach the network).

s16.apify(actorId, input)

Run an Apify actor (web scraping / enrichment, e.g. LinkedIn) and get its results.

s16.store.get(key) / set(key, value)

Per-subagent key-value store, persisted across runs.

s16.log(...args)

Append to the run log, visible in the subagent run history.

Example script

When a deal in your CRM hits the Approved status, this subagent posts a one-line summary to a Slack webhook stored in its KV store.

// Notify Slack when a deal moves to "Approved".
export default async function ({ event, s16 }) {
  if (event?.type !== 'cell.changed') return
  if (event.property !== 'Status' || event.value !== 'Approved') return

  const page = await s16.db.queryDatabase(event.databaseId, {
    filter: { pageId: event.pageId },
  })
  const deal = page.rows[0]

  const webhookUrl = await s16.store.get('slack_webhook')
  if (!webhookUrl) {
    s16.log('No slack_webhook configured, skipping.')
    return
  }

  await s16.http(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `Deal approved: ${deal.title} ($${deal.properties.Amount})`,
    }),
  })

  s16.log('Posted to Slack:', deal.title)
}

Run lifecycle

  1. 1

    Trigger fires

    Cron tick, row change, webhook hit, Gmail message, or manual click queues a run.

  2. 2

    Worker thread spins up

    WorkerPool allocates a fresh node:worker_threads sandbox with a clean vm context.

  3. 3

    Script executes

    Code runs to completion or until the memory cap, timeout, or cancel signal terminates it.

  4. 4

    Run recorded

    Output, log lines, tokens used, and abort reason land on an agent_runs row.

Full API reference

Every method on the s16 object, with signatures and examples.

Connect MCP

Plug Claude Code, Claude Desktop, or Cursor into your workspace.

Skills

Wrap an agent workflow into a reusable SKILL.md behavior pack.

Last updated June 24, 2026

Subagents · Team Brain