Scripts runtime
What the swarm-scripts runtime exposes to user code, what it does NOT expose, and how the typecheck stays aligned.
Swarm scripts are TypeScript modules persisted in the catalog via
/api/scripts/upsert and executed by the
scripts-runtime. The save-time typecheck and the runtime are deliberately
aligned — what passes the typecheck is what actually runs. This page documents
the full surface so script authors don't have to bisect.
Authoring contract
The default export always receives args first and ctx second. A
one-parameter function (ctx) treats the script arguments as ctx at runtime,
so every ctx.* access fails. For named scripts, export an argsSchema so
callers, schedules, and workflows can discover the JSON input contract:
import type { ScriptContext } from "swarm-sdk";
import * as z from "zod";
export const argsSchema = z.object({
taskId: z.string(),
limit: z.number().optional(),
});
export default async function (args: z.infer<typeof argsSchema>, ctx: ScriptContext) {
const response = await ctx.swarm.task_get({ taskId: args.taskId });
const task = ((response as { data?: unknown }).data ?? response) as {
title?: string;
};
return { title: task.title };
}Inline source passed to script-run skips the compile-time typecheck.
script-upsert typechecks before saving, so importing ScriptContext also
makes inline code promotion-safe. Use script-query-types for the authoritative
SDK and stdlib declarations. There is no ambient task context: values such as
taskId must arrive through args; the agent identity is propagated
automatically.
What the runtime provides
Scripts run inside a bun run subprocess (src/scripts-runtime/eval-harness.ts)
with the environment stripped to a small allowlist. Everything below is
available as a plain global in user code AND typechecks cleanly.
ES2022 standard library
The typecheck loads lib.es2022.d.ts. All standard ES2022 built-ins resolve:
- Primitives + boxed types:
Number,String,Boolean,Symbol,BigInt - Collections:
Array,Map,Set,WeakMap,WeakSet - Errors:
Error,TypeError,RangeError,SyntaxError,ReferenceError - Async:
Promise,Promise.all,Promise.allSettled,Promise.race,Promise.any - Iteration:
Iterator,IterableIterator,Generator - Reflection:
Object,Reflect,Proxy - JSON / Math / Date / RegExp
- Free functions:
isFinite,isNaN,parseInt,parseFloat,encodeURIComponent,decodeURIComponent,encodeURI,decodeURI
Array<T>, Promise<T>, Record<K, V>, and structural object types all work
as you'd expect. No any-everywhere required.
Web platform — fetch, URL, encoding, timers, abort
These are exposed by Bun's runtime and explicitly declared in the typecheck runtime-globals shim:
fetch,Request,Response,Headers,RequestInit,ResponseInit,Blob,FormDataURL,URLSearchParamssetTimeout,clearTimeout,setInterval,clearInterval,setImmediate,clearImmediate,queueMicrotaskAbortController,AbortSignalTextEncoder,TextDecoder,atob,btoa,structuredClonecrypto.randomUUID(),crypto.getRandomValues(...),crypto.subtle
The fetch available in the runtime IS the one Bun provides — same shape as
Node 18+'s undici fetch. Prefer ctx.stdlib.fetch / ctx.stdlib.fetchJson for
built-in retries; bare fetch works too.
For external APIs that need secrets, prefer
script connections for typed ctx.api /
ctx.mcp access, or the
scripts credential broker when you
truly need a hand-written fetch(). In both cases, avoid putting raw tokens in
source or args.
Console + logger
console.log / .warn / .error / .info / .debug are global. They also
hang off ctx.logger. Both write to the subprocess stderr (captured by the
executor and returned in the HTTP response's stderr field).
Node-compat surface
Bun's Node compatibility layer makes these available:
Buffer(Uint8Array-compatible, withBuffer.from(...),Buffer.concat(...), etc.)process.env— typed asRecord<string, string | undefined>. The environment is stripped to a small allowlist before the user script runs. You should NOT assume any specific env keys exist. Today's surviving keys:HOME,LANG,LC_ALL,PATH,TMPDIR,SWARM_SCRIPT_*(internal).process.platform,process.arch,process.version,process.cwd(),process.hrtime()
Swarm-specific imports
import { fetch, fetchJson, grep, glob, table, Redacted } from "stdlib";
// stdlib also flows through ctx.stdlib at runtimeimport type { ScriptContext, ScriptMain, SwarmConfig, Redacted } from "swarm-sdk";The ScriptContext passed to your default export carries:
ctx.swarm— typed SDK for in-swarm operations (memory, kv, tasks, scripts, repos, schedules). Method allowlist: seesrc/scripts-runtime/sdk-allowlist.ts.ctx.swarm.config—apiKey,agentId,mcpBaseUrl(all wrapped inRedacted<string>), plusctx.swarm.config.get("KEY")for user config. Redacted values stringify and serialize as<redacted>; never unwrap one into a return value, log line, or request body assembled by hand.ctx.api.<slug>/ctx.mcp.<slug>— typed clients for registered script connections. They exist only for configured connections; inspectObject.keys(ctx.api ?? {})orObject.keys(ctx.mcp ?? {})before use.- App-generated namespaces — each registered Swarm App contributes declarations
for its named queries and actions, including typed
app_queryoverloads, to the save-time script type environment. Inspect the effective declarations withscript-query-types; the surface follows the app definition available when the script is authored or updated. ctx.stdlib— the runtime helpers (fetch,fetchJson,grep,glob,table,Redacted).ctx.logger— Console-compatible logger; same destination as the globalconsole.
Most SDK results are wrapper objects; read response.data ?? response before
narrowing the payload. Prefer registered connections for authenticated calls:
credentials are attached server-side and do not enter script source or args.
Stored scripts are not automatically re-typechecked when an app definition or
registered connection changes. After a schema change, run script-query-types
and upsert affected scripts again so stale assumptions fail during validation
instead of at runtime.
ctx.swarm.* receives the complete scrubbed SDK response instead of the
10,000-byte model-facing MCP replacement. Both inline/named scripts and durable
workflow scripts stream these responses through a separate 64 MiB hard limit.
Crossing that limit cancels the response and throws an error asking you to
narrow or paginate the query; the runtime never silently truncates a field.
The wider boundary applies only inside the script sandbox. When a script
returns its result through script-run, that result crosses the agent-facing
MCP boundary again: responses over 10,000 serialized UTF-8 bytes are replaced
with a bounded preview or omission plus the per-agent KV spill pointer. Process
large collections inside the script and return only the compact derived value
the model needs.
Durable workflow context
Scripts launched through launch-script-run receive a different context:
ctx.run— the durable run'sid,agentId, andargsctx.step.rawLlm(label, config),ctx.step.agentTask(label, config), andctx.step.swarmScript(label, config)— replay-safe, journaled stepsctx.swarm,ctx.stdlib, andctx.logger
Durable runs do not receive ctx.api, ctx.mcp, or ctx.swarm.config. When a
durable workflow needs a registered connection, call a named script through
ctx.step.swarmScript.
Allowed bare imports
The TypeScript import allowlist (src/scripts-runtime/import-allowlist.ts):
stdlibswarm-sdkzod— for declaringexport const argsSchema = z.object({...})
import "fs" / "node:fs" / "path" / any unlisted bare specifier is
rejected at upsert and at run time.
What the runtime does NOT provide
These are rejected by the typecheck and would also fail at runtime — they do NOT exist in the script sandbox:
- DOM APIs —
window,document,localStorage,sessionStorage,HTMLElement,Event, etc. The typecheck does NOT includelib.dom.d.ts. We did this on purpose; the runtime is not a browser. - Filesystem —
fs,node:fs,node:fs/promises. Usectx.stdlib.globandctx.stdlib.grepfor read-only file inspection inworkspace-rwmode (v2 only). - Subprocess —
node:child_process,Bun.spawn. Scripts cannot shell out. - Net —
node:net, raw TCP/UDP. Outbound HTTP viafetchonly. Bun.*globals — even though Bun is the runtime, scripts cannot access theBunobject directly. Usectx.stdlibinstead.
Typecheck diagnostics
When script-upsert rejects code, the response is structured:
{
"error": "typecheck_failed",
"diagnostics": ["...colorized full diagnostic..."],
"structured": [
{
"severity": "error",
"code": 2552,
"message": "Cannot find name 'Mat'. Did you mean 'Math'?",
"file": "/virtual/user-script.ts",
"line": 1,
"column": 28,
"endLine": 1,
"endColumn": 31,
"identifier": "Mat",
"suggestion": "Math"
}
]
}Each entry carries the TypeScript diagnostic code (TS2552, TS2304, …),
the precise location, the offending identifier when it's a name lookup, and
the compiler's "did you mean…" hint when one is offered.
Runtime errors
When a script throws at runtime, the HTTP response includes a
runtimeError field beside stderr:
{
"exitCode": 1,
"error": "eval_error",
"stderr": "Error: kaboom from line 4\n at user-script.ts:4:13",
"runtimeError": {
"name": "Error",
"message": "kaboom from line 4",
"stack": "Error: kaboom from line 4\n at .../user-script.ts:4:13\n ...",
"userFrames": [{ "file": "user-script.ts", "line": 4, "column": 13, "raw": "at user-script.ts:4:13" }],
"userScriptLine": 4,
"userScriptColumn": 13
}
}Stack frames inside the harness or node_modules are stripped from the
stderr text shown to clients (they remain in runtimeError.stack for
debugging). The user-script path is normalized to the basename
user-script.ts — absolute tmpdir paths never leak.
Exposing a script externally
A saved script can be opted in to a public HTTP endpoint —
POST /api/x/script/<id> — for callers
outside the swarm, with optional bearer auth and typed input validation
against the same argsJsonSchema this page describes.
When to escalate
If you find code that runs fine in the runtime but fails the typecheck, treat
it as a swarm bug, not a script bug. Add a probe to src/tests/scripts-typecheck.test.ts
and either expand the runtime-globals shim
(SCRIPT_RUNTIME_GLOBALS in src/be/scripts/typecheck.ts) or open an issue
with the failing snippet.
Cost & context computation
How cost and context-window numbers are computed across harness providers, and how to read the costSource / contextFormula badges in the UI.
Scripts-only mode (code-mode)
Trim the swarm MCP surface to the 8 script tools and run all coordination through the scripts SDK — when to use it, how to enable it, and what changes.