In this tutorial, you will build a Worker that answers questions by running code. A Workers AI model writes JavaScript, a Dynamic Worker runs it without network access, and the model uses the result in its answer.
When you finish, the Worker answers a question about prime numbers and returns the code the model ran:
{
"answer": "There are 9,592 prime numbers below 100,000.",
"runs": [
{
"code": "function isPrime(n){if(n<2)return false; if(n%2===0) return n===2; for(let i=3;i*i<=n;i+=2){if(n%i===0) return false;} return true;} let count=0; for(let i=2;i<100000;i++){ if(isPrime(i)) count++; } return count;",
"output": { "result": 9592, "logs": [] }
}
]
}You will learn how to:
- Run model-generated JavaScript in a Dynamic Worker with no network access and a CPU time limit.
- Give a model a tool that runs code and returns the result.
- Send code errors back to the model so it can fix its code.
- Sign up for a Cloudflare account ↗︎.
- Install
Node.js↗︎.
Node.js version manager
Use a Node version manager like Volta ↗︎ or nvm ↗︎ to avoid permission issues and change Node.js versions. Wrangler, discussed later in this guide, requires a Node version of 16.17.0 or later.
-
Create a Worker project:
npm create cloudflare@latest -- sandbox-code-interpreter --category=hello-world --type=hello-world --lang=ts --no-deploy --no-git --no-agentsyarn create cloudflare sandbox-code-interpreter --category=hello-world --type=hello-world --lang=ts --no-deploy --no-git --no-agentspnpm create cloudflare@latest sandbox-code-interpreter --category=hello-world --type=hello-world --lang=ts --no-deploy --no-git --no-agents -
Change into the project directory:
cd sandbox-code-interpreter -
Install the AI SDK ↗︎, the Workers AI provider, and Zod ↗︎:
npm i ai workers-ai-provider zodyarn add ai workers-ai-provider zodpnpm add ai workers-ai-provider zodbun add ai workers-ai-provider zod -
Replace
wrangler.jsoncto add a Workers AI binding and a Worker Loader binding:{ "$schema": "node_modules/wrangler/config-schema.json", "name": "sandbox-code-interpreter", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-10-01", "observability": { "enabled": true, }, "upload_source_maps": true, "ai": { "binding": "AI", }, "worker_loaders": [ { "binding": "LOADER", }, ], }"$schema" = "node_modules/wrangler/config-schema.json" name = "sandbox-code-interpreter" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-10-01" upload_source_maps = true [observability] enabled = true [ai] binding = "AI" [[worker_loaders]] binding = "LOADER"AIcalls Workers AI models.LOADERcreates Dynamic Workers at runtime. -
Generate types for the bindings:
npx wrangler typesyarn wrangler typespnpm wrangler types
Create src/sandbox.ts. The runJavaScript() function loads the code into a new Dynamic Worker and returns its result and logs. It throws if the code does not finish within five seconds:
const timeoutMs = 5_000;
export async function runJavaScript(env, code) {
const sandbox = env.LOADER.load({
compatibilityDate: "2026-10-01",
mainModule: "code.js",
modules: {
"code.js": `
import { WorkerEntrypoint } from "cloudflare:workers";
function format(value) {
return typeof value === "string" ? value : JSON.stringify(value);
}
export class Code extends WorkerEntrypoint {
async run() {
const logs = [];
const console = {
log: (...values) => logs.push(values.map(format).join(" ")),
};
const result = await (async () => {
${code}
})();
return { result, logs };
}
}
`,
},
globalOutbound: null,
limits: { cpuMs: 50 },
});
let timer = null;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new Error(`The code did not finish within ${timeoutMs} ms`));
}, timeoutMs);
});
try {
return await Promise.race([sandbox.getEntrypoint("Code").run(), timeout]);
} finally {
clearTimeout(timer);
}
}import type { WorkerEntrypoint } from "cloudflare:workers";
export type Output = {
result?: unknown;
logs: string[];
};
type CodeEntrypoint = WorkerEntrypoint & {
run(): Promise<Output>;
};
const timeoutMs = 5_000;
export async function runJavaScript(env: Env, code: string): Promise<Output> {
const sandbox = env.LOADER.load({
compatibilityDate: "2026-10-01",
mainModule: "code.js",
modules: {
"code.js": `
import { WorkerEntrypoint } from "cloudflare:workers";
function format(value) {
return typeof value === "string" ? value : JSON.stringify(value);
}
export class Code extends WorkerEntrypoint {
async run() {
const logs = [];
const console = {
log: (...values) => logs.push(values.map(format).join(" ")),
};
const result = await (async () => {
${code}
})();
return { result, logs };
}
}
`,
},
globalOutbound: null,
limits: { cpuMs: 50 },
});
let timer: number | null = null;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => {
reject(new Error(`The code did not finish within ${timeoutMs} ms`));
}, timeoutMs);
});
try {
return await Promise.race([
sandbox.getEntrypoint<CodeEntrypoint>("Code").run(),
timeout,
]);
} finally {
clearTimeout(timer);
}
}The Dynamic Worker limits what the generated code can do:
globalOutbound: nullmakesfetch()andconnect()throw, so the code has no network access.- The Dynamic Worker receives no
env, so the code cannot call Workers AI or any other resource your Worker can reach. limits: { cpuMs: 50 }stops code that uses more than 50 milliseconds of CPU time. Refer to Custom resource limits.- If the code waits without using CPU, such as in a long
setTimeout(), the five-second timeout stops your Worker from waiting for it. The timer runs in your Worker, so the generated code cannot change it. - Each call to
load()creates a new Dynamic Worker, so values from one run are not available in the next.
The generated code becomes the body of an async function, so it can use await and must return its result. A local console object collects console.log() output. The code can still change anything inside the Dynamic Worker, including the wrapper around it, but it cannot reach anything the Dynamic Worker does not receive.
Replace src/index.ts. The Worker gives the model a runJavaScript tool and returns the answer from the model with every piece of code the model ran:
import { generateText, isStepCount, tool } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";
import { runJavaScript } from "./sandbox";
const model = "@cf/openai/gpt-oss-120b";
const Question = z.object({ question: z.string().min(1) });
const instructions = `Answer the question.
Always call the runJavaScript tool before you answer. Do not calculate, count, or sort in your head.
Write the body of an async JavaScript function and return the value you need.
If the tool returns an error, fix the code and run it again.
Answer in one or two plain-text sentences.`;
export default {
async fetch(request, env) {
if (request.method !== "POST") {
return new Response("Send a POST request", { status: 405 });
}
const body = Question.safeParse(await request.json().catch(() => null));
if (!body.success) {
return Response.json(
{ error: "Send a JSON body with a question" },
{ status: 400 },
);
}
const workersai = createWorkersAI({ binding: env.AI });
const runs = [];
try {
const result = await generateText({
model: workersai(model),
instructions,
prompt: body.data.question,
tools: {
runJavaScript: tool({
description:
"Run JavaScript in a sandbox with no network access. The code is the body of an async function. Return a JSON-serializable value. console.log output is captured.",
inputSchema: z.object({
code: z.string().describe("Body of an async JavaScript function"),
}),
execute: async ({ code }) => {
try {
const output = await runJavaScript(env, code);
runs.push({ code, output });
return output;
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
runs.push({ code, error: message });
return { error: message };
}
},
}),
},
// Stop after five model steps, so a model that keeps failing cannot
// run code indefinitely
stopWhen: isStepCount(5),
});
return Response.json({ answer: result.text, runs });
} catch (error) {
console.error("Model request failed", error);
return Response.json(
{ error: "The model request failed. Try again later.", runs },
{ status: 502 },
);
}
},
};import { generateText, isStepCount, tool } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";
import { runJavaScript, type Output } from "./sandbox";
const model = "@cf/openai/gpt-oss-120b";
const Question = z.object({ question: z.string().min(1) });
const instructions = `Answer the question.
Always call the runJavaScript tool before you answer. Do not calculate, count, or sort in your head.
Write the body of an async JavaScript function and return the value you need.
If the tool returns an error, fix the code and run it again.
Answer in one or two plain-text sentences.`;
type Run = { code: string } & ({ output: Output } | { error: string });
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("Send a POST request", { status: 405 });
}
const body = Question.safeParse(await request.json().catch(() => null));
if (!body.success) {
return Response.json(
{ error: "Send a JSON body with a question" },
{ status: 400 },
);
}
const workersai = createWorkersAI({ binding: env.AI });
const runs: Run[] = [];
try {
const result = await generateText({
model: workersai(model),
instructions,
prompt: body.data.question,
tools: {
runJavaScript: tool({
description:
"Run JavaScript in a sandbox with no network access. The code is the body of an async function. Return a JSON-serializable value. console.log output is captured.",
inputSchema: z.object({
code: z.string().describe("Body of an async JavaScript function"),
}),
execute: async ({ code }) => {
try {
const output = await runJavaScript(env, code);
runs.push({ code, output });
return output;
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
runs.push({ code, error: message });
return { error: message };
}
},
}),
},
// Stop after five model steps, so a model that keeps failing cannot
// run code indefinitely
stopWhen: isStepCount(5),
});
return Response.json({ answer: result.text, runs });
} catch (error) {
console.error("Model request failed", error);
return Response.json(
{ error: "The model request failed. Try again later.", runs },
{ status: 502 },
);
}
},
} satisfies ExportedHandler<Env>;generateText() sends the question to the model. When the model calls runJavaScript, the AI SDK runs execute() and sends the return value back to the model. The model can then run more code or answer.
If the code throws, exceeds the CPU time limit or the timeout, or calls fetch(), execute() returns the error message instead of failing the request. The model reads the error and can fix its code.
The Worker validates the request body with Zod before it calls the model. If a Workers AI request fails, for example because of a rate limit, the Worker logs the error and responds with 502 and the code that already ran. observability in wrangler.jsonc keeps those logs in Workers Logs.
-
Start a local development server:
npx wrangler devyarn wrangler devpnpm wrangler devDuring
wrangler dev, the Dynamic Worker runs locally. Workers AI requests always run on Cloudflare and count toward your Workers AI usage. -
POST a question to the URL Wrangler prints. The default is
http://localhost:8787:curl http://localhost:8787 --json '{"question":"How many prime numbers are there below 100,000?"}'The response includes the answer and the code the model ran:
{ "answer": "There are 9,592 prime numbers below 100,000.", "runs": [ { "code": "function isPrime(n){if(n<2)return false; if(n%2===0) return n===2; for(let i=3;i*i<=n;i+=2){if(n%i===0) return false;} return true;} let count=0; for(let i=2;i<100000;i++){ if(isPrime(i)) count++; } return count;", "output": { "result": 9592, "logs": [] } } ] }The model writes different code and wording each time.
resultis9592. -
Ask a question that needs sorting:
curl http://localhost:8787 --json '{"question":"Sort these words by length, then alphabetically: pear, fig, banana, kiwi, apple, date."}'The answer lists
fig, date, kiwi, pear, apple, banana.
-
Deploy your Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
POST a question to the
workers.devURL Wrangler prints:curl https://sandbox-code-interpreter.<YOUR_SUBDOMAIN>.workers.dev --json '{"question":"How many prime numbers are there below 100,000?"}'The response has the same shape as the local response.
- Let generated code call methods that your Worker provides. Refer to Bindings.
- Let a model call typed tools from generated code. Refer to Code Mode.
- Run shell commands or other Linux tools. Refer to Run a Linux command.
- Choose a different model. Refer to Workers AI models.