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.
You need an existing Agents SDK application that uses AIChatAgent, Vite, and the AI SDK.
-
Install the Code Mode package:
npm i @cloudflare/codemodeyarn add @cloudflare/codemodepnpm add @cloudflare/codemodebun add @cloudflare/codemode -
Add a Worker Loader binding.
DynamicWorkerExecutoruses 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"}]}TOML # Set this to today's datecompatibility_date = "2026-06-25"compatibility_flags = ["nodejs_compat"][[worker_loaders]]binding = "LOADER" -
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()],});vite.config.ts 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
CodemodeRuntimefacet 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 throughctx.exports. If you do not use the plugin, add the export manually:TypeScript export { CodemodeRuntime } from "@cloudflare/codemode"; -
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),);},},};}}src/notes-connector.ts import {CodemodeConnector,type ConnectorTools,} from "@cloudflare/codemode";type Note = { id: string; text: string };export class NotesConnector extends CodemodeConnector<Env> {private storage: DurableObjectStorage;constructor(ctx: DurableObjectState, env: Env) {super(ctx, env);this.storage = ctx.storage;}override name() {return "notes";}protected override instructions() {return "Use this connector to list and create saved notes.";}protected override tools(): ConnectorTools {return {listNotes: {description: "List saved notes.",execute: async () =>(await this.storage.get<Note[]>("notes")) ?? [],},createNote: {description: "Create a saved note.",inputSchema: {type: "object",properties: { text: { type: "string" } },required: ["text"],},requiresApproval: true,execute: async (input) => {const { text } = input as { text: string };const note = { id: crypto.randomUUID(), text };const notes = (await this.storage.get<Note[]>("notes")) ?? [];await this.storage.put("notes", [...notes, note]);return note;},revert: async (_input, result) => {const { id } = result as Note;const notes = (await this.storage.get<Note[]>("notes")) ?? [];await this.storage.put("notes",notes.filter((note) => note.id !== id),);},},};}}The
name()result becomes the sandbox global, in this casenotes.requiresApproval: truepauses beforecreateNoteexecutes. The optionalrevertfunction letsruntime.rollback()compensate for an applied call.Use
McpConnectorfor MCP tools orOpenApiConnectorfor OpenAPI operations. For MCP-specific setup, refer to Use MCP tools with Code Mode. -
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();}}src/server.ts import { AIChatAgent } from "@cloudflare/ai-chat";import {createCodemodeRuntime,DynamicWorkerExecutor,type CodemodeRuntimeHandle,type ExecutionState,type PendingAction,type Snippet,} 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<Env> {#runtime(): CodemodeRuntimeHandle {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(): Promise<PendingAction[]> {return this.#runtime().pending();}@callable()async approveExecution(executionId: string) {return this.#runtime().approve({ executionId });}@callable()async rejectExecution(executionId: string, seq: number): Promise<boolean> {return this.#runtime().reject({ executionId, seq });}@callable()async rollbackExecution(executionId: string): Promise<void> {await this.#runtime().rollback({ executionId });}@callable()async executionHistory(): Promise<ExecutionState[]> {return this.#runtime().executions(20);}@callable()async saveSnippet(name: string,description: string,executionId: string,): Promise<Snippet> {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(): Promise<Snippet[]> {return this.#runtime().snippets();}}Replace the
modelimport with the existing model setup in your application.
Ask the model to list saved notes. The model receives one codemode tool and can discover connector methods inside the sandbox:
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.