Codemode
Codemode lets LLMs write and execute code that orchestrates your tools, instead of calling them one at a time. Inspired by CodeAct ↗, it works because LLMs are better at writing code than making individual tool calls — they have seen millions of lines of real-world code but only contrived tool-calling examples.
The @cloudflare/codemode package generates TypeScript type definitions from your tools, gives the LLM a single "write code" tool, and executes the generated JavaScript in a secure, isolated Worker sandbox.
Codemode is most useful when the LLM needs to:
- Chain multiple tool calls with logic between them (conditionals, loops, error handling)
- Compose results from different tools before returning
- Work with MCP servers that expose many fine-grained operations
- Perform multi-step workflows that would require many round-trips with standard tool calling
For simple, single tool calls, standard AI SDK tool calling is simpler and sufficient.
npm install @cloudflare/codemodeIf you use @cloudflare/codemode/ai, also install the ai and zod peer dependencies:
npm install ai zodUse the standard AI SDK tool() function:
import { tool } from "ai";import { z } from "zod";
const tools = { getWeather: tool({ description: "Get weather for a location", inputSchema: z.object({ location: z.string() }), execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`, }), sendEmail: tool({ description: "Send an email", inputSchema: z.object({ to: z.string(), subject: z.string(), body: z.string(), }), execute: async ({ to, subject, body }) => `Email sent to ${to}`, }),};import { tool } from "ai";import { z } from "zod";
const tools = { getWeather: tool({ description: "Get weather for a location", inputSchema: z.object({ location: z.string() }), execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`, }), sendEmail: tool({ description: "Send an email", inputSchema: z.object({ to: z.string(), subject: z.string(), body: z.string(), }), execute: async ({ to, subject, body }) => `Email sent to ${to}`, }),};createCodeTool takes your tools and an executor, and returns a single AI SDK tool:
import { createCodeTool } from "@cloudflare/codemode/ai";import { DynamicWorkerExecutor } from "@cloudflare/codemode";
const executor = new DynamicWorkerExecutor({ loader: env.LOADER,});
const codemode = createCodeTool({ tools, executor });import { createCodeTool } from "@cloudflare/codemode/ai";import { DynamicWorkerExecutor } from "@cloudflare/codemode";
const executor = new DynamicWorkerExecutor({ loader: env.LOADER,});
const codemode = createCodeTool({ tools, executor });Pass the codemode tool to streamText or generateText like any other tool. You choose the model:
import { streamText } from "ai";
const result = streamText({ model, system: "You are a helpful assistant.", messages, tools: { codemode },});import { streamText } from "ai";
const result = streamText({ model, system: "You are a helpful assistant.", messages, tools: { codemode },});When the LLM decides to use codemode, it writes an async arrow function like:
async () => { const weather = await codemode.getWeather({ location: "London" }); if (weather.includes("sunny")) { await codemode.sendEmail({ to: "team@example.com", subject: "Nice day!", body: `It's ${weather}`, }); } return { weather, notified: true };};The code runs in an isolated Worker sandbox, tool calls are dispatched back to the host via Workers RPC, and the result is returned to the LLM.
Add a worker_loaders binding to your wrangler.jsonc. This is the only binding required:
{ "$schema": "./node_modules/wrangler/config-schema.json", "worker_loaders": [ { "binding": "LOADER" } ], "compatibility_flags": [ "nodejs_compat" ]}worker_loaders = [{ binding = "LOADER" }]compatibility_flags = ["nodejs_compat"]createCodeToolgenerates TypeScript type definitions from your tools and builds a description the LLM can read.- The LLM writes an async arrow function that calls
codemode.toolName(args). - The code is normalized via AST parsing (acorn) and sent to the executor.
DynamicWorkerExecutorspins up an isolated Worker viaWorkerLoader.- Inside the sandbox, a
Proxyinterceptscodemode.*calls and routes them back to the host via Workers RPC (ToolDispatcher extends RpcTarget). - Console output (
console.log,console.warn,console.error) is captured and returned in the result.
External fetch() and connect() are blocked by default — enforced at the Workers runtime level via globalOutbound: null. Sandboxed code can only interact with the host through codemode.* tool calls.
To allow controlled outbound access, pass a Fetcher:
const executor = new DynamicWorkerExecutor({ loader: env.LOADER, globalOutbound: null, // default — fully isolated // globalOutbound: env.MY_OUTBOUND_SERVICE // route through a Fetcher});const executor = new DynamicWorkerExecutor({ loader: env.LOADER, globalOutbound: null, // default — fully isolated // globalOutbound: env.MY_OUTBOUND_SERVICE // route through a Fetcher});The typical pattern is to create the executor and codemode tool inside an Agent's message handler:
import { Agent } from "agents";import { createCodeTool } from "@cloudflare/codemode/ai";import { DynamicWorkerExecutor } from "@cloudflare/codemode";import { streamText, convertToModelMessages, stepCountIs } from "ai";
export class MyAgent extends Agent { async onChatMessage() { const executor = new DynamicWorkerExecutor({ loader: this.env.LOADER, });
const codemode = createCodeTool({ tools: myTools, executor, });
const result = streamText({ model, system: "You are a helpful assistant.", messages: await convertToModelMessages(this.state.messages), tools: { codemode }, stopWhen: stepCountIs(10), });
// Stream response back to client... }}import { Agent } from "agents";import { createCodeTool } from "@cloudflare/codemode/ai";import { DynamicWorkerExecutor } from "@cloudflare/codemode";import { streamText, convertToModelMessages, stepCountIs } from "ai";
export class MyAgent extends Agent<Env, State> { async onChatMessage() { const executor = new DynamicWorkerExecutor({ loader: this.env.LOADER, });
const codemode = createCodeTool({ tools: myTools, executor, });
const result = streamText({ model, system: "You are a helpful assistant.", messages: await convertToModelMessages(this.state.messages), tools: { codemode }, stopWhen: stepCountIs(10), });
// Stream response back to client... }}MCP tools work the same way — merge them into the tool set:
const codemode = createCodeTool({ tools: { ...myTools, ...this.mcp.getAITools(), }, executor,});const codemode = createCodeTool({ tools: { ...myTools, ...this.mcp.getAITools(), }, executor,});Tool names with hyphens or dots (common in MCP) are automatically sanitized to valid JavaScript identifiers (for example, my-server.list-items becomes my_server_list_items).
The @cloudflare/codemode/mcp export provides two functions that wrap MCP servers with Code Mode.
Wraps an existing MCP server with a single code tool. Each upstream tool becomes a typed codemode.* method inside the sandbox:
import { codeMcpServer } from "@cloudflare/codemode/mcp";import { DynamicWorkerExecutor } from "@cloudflare/codemode";
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });const server = await codeMcpServer({ server: upstreamMcp, executor });import { codeMcpServer } from "@cloudflare/codemode/mcp";import { DynamicWorkerExecutor } from "@cloudflare/codemode";
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });const server = await codeMcpServer({ server: upstreamMcp, executor });Creates an MCP server with search and execute tools from an OpenAPI spec. All $ref pointers are resolved before being passed to the sandbox, and the host-side request handler keeps authentication out of the sandbox:
import { openApiMcpServer } from "@cloudflare/codemode/mcp";import { DynamicWorkerExecutor } from "@cloudflare/codemode";
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });const server = openApiMcpServer({ spec: openApiSpec, executor, request: async ({ method, path, query, body }) => { // Runs on the host — add auth headers here const res = await fetch(`https://api.example.com${path}`, { method, headers: { Authorization: `Bearer ${token}` }, body: body ? JSON.stringify(body) : undefined, }); return res.json(); },});import { openApiMcpServer } from "@cloudflare/codemode/mcp";import { DynamicWorkerExecutor } from "@cloudflare/codemode";
const executor = new DynamicWorkerExecutor({ loader: env.LOADER });const server = openApiMcpServer({ spec: openApiSpec, executor, request: async ({ method, path, query, body }) => { // Runs on the host — add auth headers here const res = await fetch(`https://api.example.com${path}`, { method, headers: { Authorization: `Bearer ${token}` }, body: body ? JSON.stringify(body) : undefined, }); return res.json(); },});The Executor interface is deliberately minimal — implement it to run code in any sandbox:
interface Executor { execute( code: string, fns: Record<string, (...args: unknown[]) => Promise<unknown>>, ): Promise<ExecuteResult>;}
interface ExecuteResult { result: unknown; error?: string; logs?: string[];}DynamicWorkerExecutor is the built-in Cloudflare Workers implementation. You can build your own for Node VM, QuickJS, containers, or any other sandbox.
Returns an AI SDK compatible Tool.
| Option | Type | Default | Description |
|---|---|---|---|
tools | ToolSet | ToolDescriptors | required | Your tools (AI SDK tool() or raw descriptors) |
executor | Executor | required | Where to run the generated code |
description | string | auto-generated | Custom tool description. Use \{\{types\}\} for type defs |
Executes code in an isolated Cloudflare Worker via WorkerLoader.
| Option | Type | Default | Description |
|---|---|---|---|
loader | WorkerLoader | required | Worker Loader binding from env.LOADER |
timeout | number | 30000 | Execution timeout in ms |
globalOutbound | Fetcher | null | null | Network access control. null = blocked, Fetcher = routed |
modules | Record<string, string> | — | Custom ES modules available in the sandbox. Keys are specifiers, values are source. |
Code and tool names are normalized and sanitized internally — you do not need to call normalizeCode() or sanitizeToolName() before passing them to execute().
Generates TypeScript type definitions from your tools. Used internally by createCodeTool but exported for custom use (for example, displaying types in a frontend).
import { generateTypes } from "@cloudflare/codemode/ai";
const types = generateTypes(myTools);// Returns:// type CreateProjectInput = { name: string; description?: string }// declare const codemode: {// createProject: (input: CreateProjectInput) => Promise<unknown>;// }import { generateTypes } from "@cloudflare/codemode/ai";
const types = generateTypes(myTools);// Returns:// type CreateProjectInput = { name: string; description?: string }// declare const codemode: {// createProject: (input: CreateProjectInput) => Promise<unknown>;// }For JSON Schema inputs that do not depend on the AI SDK, use the main entry point:
import { generateTypesFromJsonSchema } from "@cloudflare/codemode";
const types = generateTypesFromJsonSchema(jsonSchemaToolDescriptors);import { generateTypesFromJsonSchema } from "@cloudflare/codemode";
const types = generateTypesFromJsonSchema(jsonSchemaToolDescriptors);Converts tool names into valid JavaScript identifiers.
import { sanitizeToolName } from "@cloudflare/codemode";
sanitizeToolName("get-weather"); // "get_weather"sanitizeToolName("3d-render"); // "_3d_render"sanitizeToolName("delete"); // "delete_"import { sanitizeToolName } from "@cloudflare/codemode";
sanitizeToolName("get-weather"); // "get_weather"sanitizeToolName("3d-render"); // "_3d_render"sanitizeToolName("delete"); // "delete_"- Code runs in isolated Worker sandboxes — each execution gets its own Worker instance.
- External network access (
fetch,connect) is blocked by default at the runtime level. - Tool calls are dispatched via Workers RPC, not network requests.
- Execution has a configurable timeout (default 30 seconds).
- Console output is captured separately and does not leak to the host.
- Tool approval (
needsApproval) is not supported yet. Tools withneedsApproval: trueexecute immediately inside the sandbox without pausing for approval. Support for approval flows within codemode is planned. For now, do not pass approval-required tools tocreateCodeTool— use them through standard AI SDK tool calling instead. - Requires Cloudflare Workers environment for
DynamicWorkerExecutor. - Limited to JavaScript execution.
- LLM code quality depends on prompt engineering and model capability.