Skip to content

Create a durable Code Mode runtime

This guide adds a durable Code Mode runtime to an Agents SDK application. The runtime stores execution history, pending approvals, and snippets across Durable Object hibernation.

Prerequisites

You need an existing Agents SDK application that uses AIChatAgent, Vite, and the AI SDK.

Integrate Code Mode

  1. Install the Code Mode package:

    npm i @cloudflare/codemode
  2. Add a Worker Loader binding. DynamicWorkerExecutor uses this binding to run model-generated code in isolated Workers:

    JSONC
    {
    "$schema": "./node_modules/wrangler/config-schema.json",
    // Set this to today's date
    "compatibility_date": "2026-06-25",
    "compatibility_flags": [
    "nodejs_compat"
    ],
    "worker_loaders": [
    {
    "binding": "LOADER"
    }
    ]
    }
  3. Add the Agents and Code Mode plugins to vite.config.ts:

    vite.config.js
    import { cloudflare } from "@cloudflare/vite-plugin";
    import codemode from "@cloudflare/codemode/vite";
    import agents from "agents/vite";
    import { defineConfig } from "vite";
    export default defineConfig({
    plugins: [agents(), codemode(), cloudflare()],
    });

    The plugin exports the CodemodeRuntime facet class from your Worker entry module. The runtime stores execution state in a Durable Object facet, and the Workers runtime requires facet classes to be available through ctx.exports. If you do not use the plugin, add the export manually:

    TypeScript
    export { CodemodeRuntime } from "@cloudflare/codemode";
  4. Create a connector. Connectors are plain classes — they need no special file name or import syntax. This example stores notes in the Agent's Durable Object storage:

    src/notes-connector.js
    import { CodemodeConnector } from "@cloudflare/codemode";
    export class NotesConnector extends CodemodeConnector {
    storage;
    constructor(ctx, env) {
    super(ctx, env);
    this.storage = ctx.storage;
    }
    name() {
    return "notes";
    }
    instructions() {
    return "Use this connector to list and create saved notes.";
    }
    tools() {
    return {
    listNotes: {
    description: "List saved notes.",
    execute: async () => (await this.storage.get("notes")) ?? [],
    },
    createNote: {
    description: "Create a saved note.",
    inputSchema: {
    type: "object",
    properties: { text: { type: "string" } },
    required: ["text"],
    },
    requiresApproval: true,
    execute: async (input) => {
    const { text } = input;
    const note = { id: crypto.randomUUID(), text };
    const notes = (await this.storage.get("notes")) ?? [];
    await this.storage.put("notes", [...notes, note]);
    return note;
    },
    revert: async (_input, result) => {
    const { id } = result;
    const notes = (await this.storage.get("notes")) ?? [];
    await this.storage.put(
    "notes",
    notes.filter((note) => note.id !== id),
    );
    },
    },
    };
    }
    }

    The name() result becomes the sandbox global, in this case notes. requiresApproval: true pauses before createNote executes. The optional revert function lets runtime.rollback() compensate for an applied call.

    Use McpConnector for MCP tools or OpenApiConnector for OpenAPI operations. For MCP-specific setup, refer to Use MCP tools with Code Mode.

  5. Import the connector and create a runtime in your Agent:

    src/server.js
    import { AIChatAgent } from "@cloudflare/ai-chat";
    import {
    createCodemodeRuntime,
    DynamicWorkerExecutor,
    } from "@cloudflare/codemode";
    import { callable } from "agents";
    import { convertToModelMessages, stepCountIs, streamText } from "ai";
    import { NotesConnector } from "./notes-connector";
    import { model } from "./model";
    export class Chat extends AIChatAgent {
    #runtime() {
    return createCodemodeRuntime({
    ctx: this.ctx,
    executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }),
    connectors: [new NotesConnector(this.ctx, this.env)],
    });
    }
    async onChatMessage() {
    const result = streamText({
    model,
    messages: await convertToModelMessages(this.messages),
    tools: { codemode: this.#runtime().tool() },
    stopWhen: stepCountIs(10),
    });
    return result.toUIMessageStreamResponse();
    }
    @callable()
    async pendingApprovals() {
    return this.#runtime().pending();
    }
    @callable()
    async approveExecution(executionId) {
    return this.#runtime().approve({ executionId });
    }
    @callable()
    async rejectExecution(executionId, seq) {
    return this.#runtime().reject({ executionId, seq });
    }
    @callable()
    async rollbackExecution(executionId) {
    await this.#runtime().rollback({ executionId });
    }
    @callable()
    async executionHistory() {
    return this.#runtime().executions(20);
    }
    @callable()
    async saveSnippet(name, description, executionId) {
    const runtime = this.#runtime();
    const execution = (await runtime.executions()).find(
    (item) => item.id === executionId,
    );
    if (execution?.status !== "completed") {
    throw new Error("Only completed executions can be saved as snippets.");
    }
    return runtime.saveSnippet(name, { description, executionId });
    }
    @callable()
    async snippets() {
    return this.#runtime().snippets();
    }
    }

    Replace the model import with the existing model setup in your application.

Verify the integration

Ask the model to list saved notes. The model receives one codemode tool and can discover connector methods inside the sandbox:

JavaScript
async () => {
const matches = await codemode.search("list saved notes");
const docs = await codemode.describe(matches.results[0].path);
const savedNotes = await notes.listNotes();
return { docs, savedNotes };
};

When the model calls notes.createNote(), the execution pauses. Use pendingApprovals() to show the pending action. Pass its executionId to approveExecution(), or pass both executionId and seq to rejectExecution().

Approval resumes the same script through replay. Completed calls return recorded results instead of running again. Rejection ends the paused execution without undoing earlier actions.

Call rollbackExecution() to compensate for applied calls whose currently configured connector provides revert. Save only completed executions as snippets.