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.
The generated types
Section titled “The generated types”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.
Describing actions individually
Section titled “Describing actions individually”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.
Checking happens automatically
Section titled “Checking happens automatically”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.
Handling ScriptCompileError
Section titled “Handling ScriptCompileError”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'.
Surfacing diagnostics to an author
Section titled “Surfacing diagnostics to an author”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 messagenext 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 relevantdescribeActions()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.