Code interpreter
Create code contexts bound to a runtime Worker and run code with the typed client.
Mode: sandbox.interpreter.* is stateful mode; the free runCode() function (below) is stateless mode. See Stateless mode and Stateful mode.
sandbox.interpreter is always present on a SandboxClient — there is no attach step or subclass. A code context is a durable, named REPL bound to one runtime Worker by the name of a Service Binding in your own environment; running code in the same context lets top-level variables, functions, classes, and imported modules from one execution stay visible to the next. There is no language option anywhere: the binding determines the language. See Code contexts for how this works under the hood, and Use code contexts for a task-oriented walkthrough.
Code contexts are supported for JavaScript, Python, and Perl. Ruby does not support code contexts — GET /interpreter on a Ruby runtime Worker always reports contexts: false, the same as any runtime Worker deployed with --stateless. Against such a binding, createCodeContext() fails with ValidationFailedError, and runCode(code, { binding }) (no context) falls back to running statelessly instead.
Methods
createCodeContext()
Create a new code context.
await sandbox.interpreter.createCodeContext(options: CreateContextOptions): Promise<CodeContext>
Parameters (CreateContextOptions):
binding— required. The name of a Service Binding, in your own environment, to a sandbox-workers runtime Worker. Must match/^[A-Za-z_][A-Za-z0-9_]*$/, name a binding that exists and has afetchmethod, and answerGET /interpreterwith{ language, engine, contexts: true }— otherwise this throwsValidationFailedError(“Unknown binding ‘X’”, “Binding ‘X’ is not a sandbox-workers runtime Worker”, or “Code contexts are not supported by binding ‘X’ (language)”). This probe runs only oncreateCodeContextand default-context creation, never on every execution. Typed asServiceBindingName<Env>(see Types) whensandboxcame fromgetSandbox<Env>(), otherwise plainstring.cwd— the context’s initial working directory. Defaults to/workspace.envVars— environment variables layered onto executions in this context (Record<string, string | undefined>; a value ofundefinedis dropped, not sent).
Returns: Promise<CodeContext> — see Types.
const ctx = await sandbox.interpreter.createCodeContext({
binding: "PYTHON",
cwd: "/workspace/project",
envVars: { API_KEY: env.API_KEY },
});
A sandbox holds at most 8 code contexts across all bindings, with one interpreter resident in memory per runtime Worker; the rest are restored from their stored snapshot on next use.
listCodeContexts()
List every code context currently in the sandbox, across every binding.
await sandbox.interpreter.listCodeContexts(): Promise<CodeContext[]>
Parameters: none.
Returns: Promise<CodeContext[]>.
const contexts = await sandbox.interpreter.listCodeContexts();
deleteCodeContext()
Delete a code context: its live interpreter and its stored memory snapshot on that binding’s runtime Worker. /workspace is untouched, since it belongs to the sandbox, not the context.
await sandbox.interpreter.deleteCodeContext(id: string): Promise<void>
Parameters:
id— the context’s id, fromCodeContext.id.
Returns: Promise<void>.
await sandbox.interpreter.deleteCodeContext(ctx.id);
Deleting an unknown id fails with ContextNotFoundError. This is also the way to compact a context whose memory has grown: delete it and create a new one, since a context’s linear memory never shrinks on its own. See Memory snapshots.
runCode()
Execute code, in a context or statelessly.
await sandbox.interpreter.runCode(code: string, options?: RunCodeOptions): Promise<ExecutionResult>
Parameters:
code— the script to run. The value of its last top-level expression is the result. Limited to 64 KiB UTF-8.options(optional,RunCodeOptions):context— theCodeContextto run in (fromcreateCodeContext()orlistCodeContexts()).binding— the Service Binding name to use whencontextis omitted: runs in (or creates) the default context for that binding — the oldest existing context with thatbinding, or a fresh one atcwd: "/workspace"when none exists. For a binding that reportscontexts: false(a stateless-only runtime Worker), this runs statelessly through the runtime’s plainPOST /executeinstead — no context, no/workspace, and the result has nocontextfield. Typed asServiceBindingName<Env>(see Types) whensandboxcame fromgetSandbox<Env>(), otherwise plainstring.- Passing neither
contextnorbindingfails withValidationFailedError(“Pass a context or a binding”). envVars— environment variables for this call (Record<string, string | undefined>;undefinedunsets a key for this call rather than being sent).timeout— a request timeout in milliseconds; internally buildsAbortSignal.timeout(timeout). The guest is still separately bounded by its fuel budget regardless oftimeout.signal— anAbortSignalto cancel the request; combined with atimeout-derived signal (viaAbortSignal.any) when both are given.onStdout—(output: { text: string; timestamp: number }) => void | Promise<void>, called once per stdout line.onStderr— same shape asonStdout, for stderr.onResult—(result: { text?: string; json?: JsonValue; formats(): string[] }) => void | Promise<void>, called once per entry inresults(at most one).onError—(error: ExecutionError) => void | Promise<void>, called when the execution produced a guesterror.
Returns: Promise<ExecutionResult> — see Types.
onStdout/onStderr/onResult/onError all fire after the response has arrived, in order (stdout lines, then stderr lines, then result entries, then the error callback) — there is no streaming during execution, and no runCodeStream.
runCode always resolves, whether or not the guest code raised an error — check result.error, described below. It only throws for a binding failure or a non-200 response, as a SandboxError subclass; see Errors.
const ctx = await sandbox.interpreter.createCodeContext({ binding: "PYTHON" });
await sandbox.interpreter.runCode("radius = 5", { context: ctx });
const result = await sandbox.interpreter.runCode("import math\nmath.pi * radius ** 2", {
context: ctx,
});
console.log(result.results[0]); // { text: "78.53981633974483" }
runCode() (free function, stateless mode)
Run code statelessly against a runtime Worker, without a sandbox or a code context: a fresh Wasm instance per call, no files, no getSandbox involved.
import { runCode } from "@sandbox-workers/core";
await runCode(target: ServiceBindingTarget | SandboxNamespace, code: string, options?: StatelessRunCodeOptions): Promise<ExecutionResult>
Parameters:
target— must be a Service Binding (Fetcher) to the runtime Worker. Passing aSandboxDurable Object namespace throws synchronously (a plainError, not a rejected promise): usegetSandbox(namespace, id).interpreter.runCode()for that instead, since there is no sandbox id to route through here.code— same assandbox.interpreter.runCode()’scode.options(optional):StatelessRunCodeOptions, i.e.RunCodeOptionsminuscontext/binding—envVars,timeout,signal,onStdout,onStderr,onResult,onError. There is nocontextorbindingoption; a stateless call has no code context and no sandbox to route a binding name through.
Returns: Promise<ExecutionResult> — see Types. Guest errors set result.error rather than throwing, exactly like sandbox.interpreter.runCode(); the callbacks fire in the same order, after the response arrives.
import { runCode } from "@sandbox-workers/core";
const result = await runCode(env.PYTHON, "import os\nint(os.environ['X']) ** 2", {
envVars: { X: "12" },
});
This is the stateless counterpart above: it posts directly to the runtime Worker’s POST /execute (see HTTP API), so there is no sandbox id, no context, and no persistence between calls.
sandbox.setEnvVars()
Layer environment variables onto the sandbox, visible to every context regardless of binding. This lives on sandbox itself, not on sandbox.interpreter.
await sandbox.setEnvVars(envVars: Record<string, string | undefined>): Promise<void>
Parameters:
envVars— keys to set or unset. A value ofundefinedunsets that key (sent to the sandbox asnull); any other value sets it.
Returns: Promise<void>.
await sandbox.setEnvVars({ NAME: "value", OLD: undefined }); // unsets OLD
See Environment variables for how sandbox-level, setEnvVars(), and per-call envVars layer together.
Types
CodeContext
interface CodeContext {
readonly id: string;
readonly binding: string;
readonly language: string;
readonly cwd: string;
readonly createdAt: Date;
readonly lastUsed: Date;
}
binding is the Service Binding name the context was created against; language is that binding’s runtime language, reported by GET /interpreter. binding here is always plain string — it comes back from the Durable Object, not from your own Env.
ServiceBindingName<Env>
type ServiceBindingName<Env> = {
[K in keyof Env & string]: Env[K] extends ServiceBindingTarget ? K : never;
}[keyof Env & string];
The names of the Service Bindings (values with a fetch method) in an Env type; this is what binding options are narrowed to when sandbox came from getSandbox<Env>().
ExecutionResult
The value every runCode() call resolves to:
{
"code": "const x = Number(process.env.X);\nx ** 2",
"language": "javascript",
"engine": "SpiderMonkey 147 / goccy spidermonkey-wasm v0.2.6",
"durationMs": 8,
"logs": { "stdout": [], "stderr": [] },
"results": [{ "text": "144" }],
"usage": {
"fuelConsumed": 1800000,
"fuelLimit": 50000000,
"memoryBytes": 34668544
}
}
interface ExecutionResult {
code: string;
language: string;
engine: string;
durationMs: number;
logs: { stdout: string[]; stderr: string[] };
results: Array<{ text?: string; json?: JsonValue }>;
error?: { name: string; message: string; traceback: string[]; lineNumber?: number };
executionCount?: number;
usage?: { fuelConsumed: number; fuelLimit: number; memoryBytes: number };
context?: { id: string; cwd: string; executions: number; snapshotMs?: number; expiresAt?: number };
}
language/engineare always the runtime Worker’s own (e.g."javascript") — there is no per-request language selection.durationMsis elapsed engine execution time, not a billing measurement.usageis absent when the engine failed before metering was available.erroris present only for a guest-side failure — a raised exception, or a fuel/output/result limit. It is never thrown; checkresult.errorinstead.tracebackis the language’s own stack trace, as lines of text.contextis present only when the execution ran in a code context (a stateless call, including one that fell back from acontexts: falsebinding, omits it).snapshotMsis present only on an execution that actually wrote a memory snapshot.expiresAtreflects the sandbox’s idle-expiry deadline as of this request, and is omitted when expiry is disabled.
A guest error looks like this instead — logs produced before the error are still returned, and results is empty:
{
"code": "1 / 0",
"language": "python",
"engine": "CPython 3.14.6",
"durationMs": 5,
"logs": { "stdout": [], "stderr": [] },
"results": [],
"error": {
"name": "ZeroDivisionError",
"message": "division by zero",
"traceback": ["Traceback (most recent call last):", "..."]
}
}
Result mapping
results has at most one entry, chosen from the value of the last top-level expression:
| Value | Entry |
|---|---|
JS undefined, Python None, Ruby nil, Perl undef |
none — results is [] |
| Container (JS object/array, Python dict/list, Ruby Hash/Array, Perl HASH/ARRAY ref) | { "json": ... } |
| Anything else | { "text": "..." }, the language’s native string representation |
If a container fails to serialize as JSON, it falls back to a text entry using the same native representation.
Limits
Code is limited to 64 KiB UTF-8; the complete request (code plus envVars) is limited to 96 KiB. Fuel, console-output, and result-size limits are reported as a normal (HTTP 200) result with error.name set to "ExecutionLimitError", rather than by throwing — console output produced before the limit was hit is still returned in logs. See Limits for the exact fuel and output caps per language.