Skip to content

Execute a script

sandbox.eval runs one TypeScript script in an isolated runtime and returns everything it produced. This guide covers the contract in depth: what you pass in, what a script can reach, and what you get back.

const result = await sandbox.eval({
script: 'const r = await add({ a: 1, b: 2 }); data.sum = r.sum;',
data: {},
});

eval takes a single options object:

Field Type Description
script string TypeScript source, executed in a QuickJS isolate. Type-checked before it runs.
data Record<string, unknown> A persistent object the script reads and writes, like a notebook kernel’s state.
callLog (CallLogEntry | null)[] (optional) A prior run’s log, passed back to resume or replay. See Resume an interrupted script.
resumption { position, data, state? } (optional) The answer to a previous run’s interrupt.
logger Logger (optional) Passed through to every tool’s execute.

This guide covers a plain run. callLog and resumption are the mechanics of resuming an interrupted run and are covered separately.

The script is authored in TypeScript. Before it runs, the sandbox type-checks it against the generated action signatures and the data global; if that fails, eval throws ScriptCompileError and nothing executes (see Type-check scripts). It is then transpiled to JavaScript and run.

The script body may use top-level await — the sandbox wraps it in an async function — so you can await action calls directly and coordinate several with Promise.all.

The runtime is sealed: no network, no filesystem, no import. The only things a script can reach are the globals the sandbox installs — data, console, and one function per action.

Whatever you pass as data is injected as a global the script can read, mutate, or replace wholesale:

// Reads the incoming value and writes derived state back.
await sandbox.eval({
script: 'data.doubled = data.value * 2;',
data: { value: 21 },
});
// result.data => { value: 21, doubled: 42 }
// A script may replace `data` entirely.
await sandbox.eval({
script: 'data = { replaced: true };',
data: { old: 1 },
});
// result.data => { replaced: true }

data is the run’s working memory, not its output. Its contents are never surfaced to a caller directly — a script exposes what it wants seen by logging it. Pass the previous run’s returned data back in as the next run’s data to carry state forward, notebook-style.

Each entry in the Sandbox’s actions map is a tool and becomes a global async function named by its key. The argument is the tool’s input; the returned promise resolves to its output:

const sandbox = new Sandbox({ actions: { add, multiply } });
await sandbox.eval({
script: 'const r = await add({ a: 2, b: 3 }); data.sum = r.sum;',
data: {},
});

The calls return real promises, so a script can run independent actions concurrently:

await sandbox.eval({
script: `
const results = await Promise.all([
fetchNote({ id: 'a' }),
fetchNote({ id: 'b' }),
fetchNote({ id: 'c' }),
]);
data.titles = results.map((r) => r.title);
`,
data: {},
});

If an action’s execute rejects, the rejection surfaces inside the script as a normal thrown error the script can try/catch. If it rejects because the script itself throws, that error propagates out of eval.

The sandbox installs a console whose log, info, warn, error, and debug calls are captured host-side rather than written to stdout. Each becomes a SandboxConsoleEntry:

type SandboxConsoleEntry = {
level: 'log' | 'info' | 'warn' | 'error' | 'debug';
message: string;
};

Arguments are formatted into a single string per call, in call order:

const { console: output } = await sandbox.eval({
script: "console.log('hello', { a: 1 }); console.error('bad', 2);",
data: {},
});
// output => [
// { level: 'log', message: 'hello {"a":1}' },
// { level: 'error', message: 'bad 2' },
// ]

Because data is internal, console is the script’s output channel — the way a run reports its results.

eval resolves to a SandboxEvalResult:

type SandboxEvalResult = {
data: Record<string, unknown>; // the persistent object after the run
callLog: (CallLogEntry | null)[]; // the ordered record of the run
console: SandboxConsoleEntry[]; // everything logged, in order
};

Practical notes:

  • Read data for state, console for output. Feed the returned data back into the next eval to continue where you left off.
  • Keep the callLog if you may need to resume or replay the run — it is the only thing that makes that possible. See Determinism and resumption.
  • Runs are isolated. Each eval gets a fresh context; globals a script sets leak into no other run. Only data, callLog, and console cross the boundary.