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.
Fires when you click Run, or when an MCP tool calls s16_run_subagent.
Use it for: Ad-hoc runs, debugging, one-shot scripts.
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.
Fires on a schedule using standard cron syntax.
Use it for: Daily digests, hourly sync jobs, weekly reports.
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.
Fires when an email arrives at a connected Gmail account.
Use it for: Inbox triage, ticket creation, lead capture.
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
Trigger fires
Cron tick, row change, webhook hit, Gmail message, or manual click queues a run.
- 2
Worker thread spins up
WorkerPool allocates a fresh node:worker_threads sandbox with a clean vm context.
- 3
Script executes
Code runs to completion or until the memory cap, timeout, or cancel signal terminates it.
- 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