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.
1. Define two tools
Section titled “1. Define two tools”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.
2. Build a Sandbox
Section titled “2. Build a Sandbox”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(...).
3. Run a script
Section titled “3. Run a script”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.
4. Inspect what came back
Section titled “4. Inspect what came back”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 } },// ]datais 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.consoleis everything the script logged, in order, each entry tagged with a level (log,info,warn,error,debug). This is the run’s output channel.callLogis the ordered record of every action call (with its input and response) and everyDate.now()/Math.random()read. It is what makes a run replayable.
What happened
Section titled “What happened”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.
Next steps
Section titled “Next steps”- 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
evalcontract. For the full detail onscript,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.