Skip to content

Run a sandbox script

The sandbox runs a TypeScript script in an isolated runtime and gives it a set of tools as ordinary async functions. In this lesson you will build a Sandbox with two small tools, run a script that calls them and writes to a shared data object, and then read back everything the run produced. By the end you will understand the three things a run returns — data, console, and callLog — and be ready for type-checking and resumption.

A sandbox runs tools — transport-neutral callables with typed input and output. Define two tiny ones, add and multiply, with defineTool. A tool’s execute takes the input directly (and a context object as its second argument, unused here):

import { defineTool, z } from '@grundlag/core';
const add = defineTool({
id: 'add',
name: 'Add two numbers',
input: z.object({ a: z.number(), b: z.number() }),
output: z.object({ sum: z.number() }),
execute: async (input) => ({ sum: input.a + input.b }),
});
const multiply = defineTool({
id: 'multiply',
name: 'Multiply two numbers',
input: z.object({ a: z.number(), b: z.number() }),
output: z.object({ product: z.number() }),
execute: async (input) => ({ product: input.a * input.b }),
});

We define tools directly here to keep the lesson self-contained. In a real app the tools usually come from a host’s providers via providers.toTools() — a provider action becomes a tool with its dependencies bound in. See Tools and transports.

A Sandbox takes a map of tools. The key you give each tool is the name it will be exposed under inside a script — no service container needed, since each tool already carries whatever it depends on.

import { Sandbox } from '@grundlag/sandbox';
const sandbox = new Sandbox({
actions: { add, multiply },
});

Because the keys are add and multiply, the script will call add(...) and multiply(...).

Call sandbox.eval with a script (TypeScript source) and a data object. Inside the script each action is a global async function; data is a mutable object you read and write; and console.log output is captured for you.

const result = await sandbox.eval({
script: `
const first = await add({ a: 2, b: 3 });
const scaled = await multiply({ a: first.sum, b: 10 });
data.answer = scaled.product;
console.log('computed answer', data.answer);
`,
data: {},
});

The script is written in TypeScript. It is type-checked against the action signatures before it runs (more on that below), then executed in an isolated QuickJS runtime — no network, no filesystem, no imports, only the globals the sandbox declares.

eval resolves to an object with three fields:

console.log(result.data);
// { answer: 50 }
console.log(result.console);
// [ { level: 'log', message: 'computed answer 50' } ]
console.log(result.callLog);
// [
// { type: 'action', name: 'add', input: { a: 2, b: 3 }, response: { sum: 5 } },
// { type: 'action', name: 'multiply', input: { a: 5, b: 10 }, response: { product: 50 } },
// ]
  • data is the persistent state the script wrote to. It is the run’s working memory, not its output channel — nothing on it is shown to a caller unless the script logs it.
  • console is everything the script logged, in order, each entry tagged with a level (log, info, warn, error, debug). This is the run’s output channel.
  • callLog is the ordered record of every action call (with its input and response) and every Date.now() / Math.random() read. It is what makes a run replayable.

You gave the sandbox two typed actions, and a script called them like local functions, awaiting each result. The sandbox validated the script, ran it in isolation, threaded your data object through, captured the console output, and recorded every action call in the call log. Nothing escaped the runtime except the values you can see in the result.

  • Type-checking. Every script is type-checked against the generated action types before it runs — a mistyped argument or a call to an action that does not exist fails the run before any code executes. See Type-check scripts.
  • The eval contract. For the full detail on script, data, the console channel, and the result shape, see Execute a script.
  • Resumption. An action can pause a run to ask a human for input. The sandbox turns that into a resumable checkpoint you can persist and continue later. See Resume an interrupted script and the concept behind it, Determinism and resumption.