Skip to content

Codemode

Beta

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.

When to use Codemode

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.

Installation

Terminal window
npm install @cloudflare/codemode

If you use @cloudflare/codemode/ai, also install the ai and zod peer dependencies:

Terminal window
npm install ai zod

Quick start

1. Define your tools

Use the standard AI SDK tool() function:

JavaScript
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}`,
}),
};

2. Create the codemode tool

createCodeTool takes your tools and an executor, and returns a single AI SDK tool:

JavaScript
import { createCodeTool } from "@cloudflare/codemode/ai";
import { DynamicWorkerExecutor } from "@cloudflare/codemode";
const executor = new DynamicWorkerExecutor({
loader: env.LOADER,
});
const codemode = createCodeTool({ tools, executor });

3. Use with streamText

Pass the codemode tool to streamText or generateText like any other tool. You choose the model:

JavaScript
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:

JavaScript
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.

Configuration

Wrangler bindings

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"
]
}

How it works

  1. createCodeTool generates TypeScript type definitions from your tools and builds a description the LLM can read.
  2. The LLM writes an async arrow function that calls codemode.toolName(args).
  3. The code is normalized via AST parsing (acorn) and sent to the executor.
  4. DynamicWorkerExecutor spins up an isolated Worker via WorkerLoader.
  5. Inside the sandbox, a Proxy intercepts codemode.* calls and routes them back to the host via Workers RPC (ToolDispatcher extends RpcTarget).
  6. Console output (console.log, console.warn, console.error) is captured and returned in the result.

Network isolation

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:

JavaScript
const executor = new DynamicWorkerExecutor({
loader: env.LOADER,
globalOutbound: null, // default — fully isolated
// globalOutbound: env.MY_OUTBOUND_SERVICE // route through a Fetcher
});

Using with an Agent

The typical pattern is to create the executor and codemode tool inside an Agent's message handler:

JavaScript
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...
}
}

With MCP tools

MCP tools work the same way — merge them into the tool set:

JavaScript
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).

MCP server wrappers

The @cloudflare/codemode/mcp export provides two functions that wrap MCP servers with Code Mode.

codeMcpServer

Wraps an existing MCP server with a single code tool. Each upstream tool becomes a typed codemode.* method inside the sandbox:

JavaScript
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 });

openApiMcpServer

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:

JavaScript
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

The Executor interface is deliberately minimal — implement it to run code in any sandbox:

TypeScript
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.

API reference

createCodeTool(options)

Returns an AI SDK compatible Tool.

OptionTypeDefaultDescription
toolsToolSet | ToolDescriptorsrequiredYour tools (AI SDK tool() or raw descriptors)
executorExecutorrequiredWhere to run the generated code
descriptionstringauto-generatedCustom tool description. Use \{\{types\}\} for type defs

DynamicWorkerExecutor

Executes code in an isolated Cloudflare Worker via WorkerLoader.

OptionTypeDefaultDescription
loaderWorkerLoaderrequiredWorker Loader binding from env.LOADER
timeoutnumber30000Execution timeout in ms
globalOutboundFetcher | nullnullNetwork access control. null = blocked, Fetcher = routed
modulesRecord<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().

generateTypes(tools)

Generates TypeScript type definitions from your tools. Used internally by createCodeTool but exported for custom use (for example, displaying types in a frontend).

JavaScript
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:

JavaScript
import { generateTypesFromJsonSchema } from "@cloudflare/codemode";
const types = generateTypesFromJsonSchema(jsonSchemaToolDescriptors);

sanitizeToolName(name)

Converts tool names into valid JavaScript identifiers.

JavaScript
import { sanitizeToolName } from "@cloudflare/codemode";
sanitizeToolName("get-weather"); // "get_weather"
sanitizeToolName("3d-render"); // "_3d_render"
sanitizeToolName("delete"); // "delete_"

Security considerations

  • 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.

Current limitations

  • Tool approval (needsApproval) is not supported yet. Tools with needsApproval: true execute immediately inside the sandbox without pausing for approval. Support for approval flows within codemode is planned. For now, do not pass approval-required tools to createCodeTool — 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.