agent-swarm.devagent-swarm.dev
Guides

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, FormData
  • URL, URLSearchParams
  • setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate, queueMicrotask
  • AbortController, AbortSignal
  • TextEncoder, TextDecoder, atob, btoa, structuredClone
  • crypto.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, with Buffer.from(...), Buffer.concat(...), etc.)
  • process.env — typed as Record<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 runtime
import 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: see src/scripts-runtime/sdk-allowlist.ts.
  • ctx.swarm.configapiKey, agentId, mcpBaseUrl (all wrapped in Redacted<string>), plus ctx.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; inspect Object.keys(ctx.api ?? {}) or Object.keys(ctx.mcp ?? {}) before use.
  • App-generated namespaces — each registered Swarm App contributes declarations for its named queries and actions, including typed app_query overloads, to the save-time script type environment. Inspect the effective declarations with script-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 global console.

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's id, agentId, and args
  • ctx.step.rawLlm(label, config), ctx.step.agentTask(label, config), and ctx.step.swarmScript(label, config) — replay-safe, journaled steps
  • ctx.swarm, ctx.stdlib, and ctx.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):

  • stdlib
  • swarm-sdk
  • zod — for declaring export 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 include lib.dom.d.ts. We did this on purpose; the runtime is not a browser.
  • Filesystem — fs, node:fs, node:fs/promises. Use ctx.stdlib.glob and ctx.stdlib.grep for read-only file inspection in workspace-rw mode (v2 only).
  • Subprocess — node:child_process, Bun.spawn. Scripts cannot shell out.
  • Net — node:net, raw TCP/UDP. Outbound HTTP via fetch only.
  • Bun.* globals — even though Bun is the runtime, scripts cannot access the Bun object directly. Use ctx.stdlib instead.

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.

On this page