Skip to content

Type-check scripts

Every script the sandbox runs is type-checked first — against the data global and a typed async function for each action — with no opt-out. A script that references an action that does not exist, or passes the wrong shape of input, fails before any code runs. This guide covers the types the sandbox generates and how to surface a failure to whoever (or whatever) wrote the script.

sandbox.createTypes() returns the full ambient declaration file a script is checked against: the mutable data global, the console global, and one declare function per action with named input and output interfaces derived from the action’s Zod schemas.

const types = await sandbox.createTypes();
// declare var data: Record<string, any>;
// declare var console: { log(...args: any[]): void; /* ... */ };
//
// interface AddInput { a: number; b: number; }
// interface AddOutput { sum: number; }
// declare function add(input: AddInput): Promise<AddOutput>;

data is intentionally loose (Record<string, any>) so a script can shape its working state freely; the strictly-checked surface is the action functions. Write this string to a .d.ts file for editor support, or hand it to an author as the contract they must write against.

When you want to present actions selectively — for example, to show an LLM only the tools relevant to a task — use describeActions(). It returns one entry per action pairing its identity with its own type block:

type ActionDescription = {
functionName: string; // the name the action is exposed under in scripts
id: string;
name: string;
description?: string;
types: string; // the TypeScript declarations for this action
};
const all = await sandbox.describeActions();
const some = await sandbox.describeActions(['add', 'multiply']);

Pass an array of function names to describe only those, in the order given; omit it to describe every registered action. Unknown names are ignored. createTypes() is the whole declaration file at once; describeActions() is the same information split per action so you can compose your own subset.

You do not call the type checker yourself. eval runs it up front on every script:

// This throws before any action executes — `b` must be a number.
await sandbox.eval({
script: 'await add({ a: 1, b: "nope" });',
data: {},
});
// So does this — `subtract` is not a registered action.
await sandbox.eval({
script: 'await subtract({ a: 1, b: 2 });',
data: {},
});

A type error means the run is rejected before it starts, rather than silently reading undefined from a mistyped field at runtime.

When the check fails, eval throws ScriptCompileError. It carries a diagnostics array, each entry pinned to a line and column in the script:

type ScriptDiagnostic = {
line: number;
column: number;
message: string;
};
import { ScriptCompileError } from '@grundlag/sandbox';
try {
await sandbox.eval({ script, data: {} });
} catch (error) {
if (error instanceof ScriptCompileError) {
for (const d of error.diagnostics) {
console.error(`line ${d.line}, col ${d.column}: ${d.message}`);
}
return;
}
throw error;
}

Line and column are 1-based, so they point straight at the offending token. Typical messages include Type 'string' is not assignable to type 'number' and Cannot find name 'subtract'.

The diagnostics array is designed to be fed back to whoever wrote the script so they can fix it:

  • To a person, render each diagnostic as line:column message next to the script — the same format a code editor uses.
  • To an LLM author, return the diagnostics as the tool result and ask the model to produce a corrected script. Pairing them with the output of createTypes() (or the relevant describeActions() entries) gives the model both the exact errors and the exact contract it must satisfy, which is usually enough to fix the script on the next turn. This is how wrapping the sandbox as an agent tool keeps a model on the rails.

Because checking is unconditional, a script that reaches execution is guaranteed to match the action signatures — the runtime never has to guess about a call’s shape.