Add to existing project

Last updated Oct 12, 2026 This guide shows how to add agents to an existing Cloudflare Workers project. If you are starting fresh, refer to Building a chat agent instead.

Prerequisites

  • An existing Cloudflare Workers project with a Wrangler configuration file
  • Node.js 18 or newer

1. Install the package

npmyarnpnpmbun

npm i agents
yarn add agents
pnpm add agents
bun add agents

For React applications, no additional packages are needed — React bindings are included.

For Hono applications:

npmyarnpnpmbun

npm i agents hono-agents
yarn add agents hono-agents
pnpm add agents hono-agents
bun add agents hono-agents

2. Create an Agent

Create a new file for your agent (for example, src/agents/counter.ts):

import { Agent, callable } from "agents";

export class CounterAgent extends Agent {
    initialState = { count: 0 };

@callable()
    increment() {
        this.setState({ count: this.state.count + 1 });
        return this.state.count;
    }

@callable()
    decrement() {
        this.setState({ count: this.state.count - 1 });
        return this.state.count;
    }
}
import { Agent, callable } from "agents";

export type CounterState = {
    count: number;
};

export class CounterAgent extends Agent<Env, CounterState> {
    initialState: CounterState = { count: 0 };

@callable()
    increment() {
        this.setState({ count: this.state.count + 1 });
        return this.state.count;
    }

@callable()
    decrement() {
        this.setState({ count: this.state.count - 1 });
        return this.state.count;
    }
}

3. Update Wrangler configuration

Add the Durable Object binding and migration:

{
    "name": "my-existing-project",
    "main": "src/index.ts",
    "upload_source_maps": true,
    // Set this to today's date
    "compatibility_date": "2026-10-27",
    "compatibility_flags": ["nodejs_compat"],

"durable_objects": {
        "bindings": [
            {
                "name": "CounterAgent",
                "class_name": "CounterAgent",
            },
        ],
    },

"migrations": [
        {
            "tag": "v1",
            "new_sqlite_classes": ["CounterAgent"],
        },
    ],
}
name = "my-existing-project"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"
compatibility_flags = [ "nodejs_compat" ]

[[durable_objects.bindings]]
name = "CounterAgent"
class_name = "CounterAgent"

[[migrations]]
tag = "v1"
new_sqlite_classes = [ "CounterAgent" ]

Key points:

  • name in bindings becomes the property on env (for example, env.CounterAgent)
  • class_name must exactly match your exported class name
  • new_sqlite_classes enables SQLite storage for state persistence
  • nodejs_compat flag is required for the agents package

4. Configure TypeScript and Vite

If you use @callable() decorators (as in the example above), you need two build configurations.

tsconfig.json — extend agents/tsconfig (or set "target": "ES2021" manually):

{
    "extends": "agents/tsconfig"
}

If you have an existing tsconfig.json with custom settings, you can extend and override:

{
    "extends": "agents/tsconfig",
    "compilerOptions": {
        "paths": { "~/*": ["./src/*"] }
    }
}

vite.config.ts — add the agents() plugin (handles TC39 decorator transforms for Vite 8):

import agents from "agents/vite";

export default defineConfig({
    plugins: [
        agents(),
        // ... your existing plugins
    ],
});
import agents from "agents/vite";

export default defineConfig({
    plugins: [
        agents(),
        // ... your existing plugins
    ],
});

If your project does not use Vite, the tsconfig.json change alone is sufficient — your bundler must support TC39 decorators (stage 3, version 2023-11).

For more details, refer to the TypeScript configuration and Vite configuration reference.

5. Export the Agent class

Your agent class must be exported from your main entry point. Update your src/index.ts:

// Export the agent class (required for Durable Objects)
export { CounterAgent } from "./agents/counter";

// Your existing exports...
export default {
    // ...
};
// Export the agent class (required for Durable Objects)
export { CounterAgent } from "./agents/counter";

// Your existing exports...
export default {
    // ...
} satisfies ExportedHandler<Env>;

6. Wire up routing

Choose the approach that matches your project structure:

Plain Workers (fetch handler)

import { routeAgentRequest } from "agents";
export { CounterAgent } from "./agents/counter";

export default {
    async fetch(request, env, ctx) {
        // Try agent routing first
        const agentResponse = await routeAgentRequest(request, env);
        if (agentResponse) return agentResponse;

// Your existing routing logic
        const url = new URL(request.url);
        if (url.pathname === "/api/hello") {
            return Response.json({ message: "Hello!" });
        }

return new Response("Not found", { status: 404 });
    },
};
import { routeAgentRequest } from "agents";
export { CounterAgent } from "./agents/counter";

export default {
    async fetch(request: Request, env: Env, ctx: ExecutionContext) {
        // Try agent routing first
        const agentResponse = await routeAgentRequest(request, env);
        if (agentResponse) return agentResponse;

// Your existing routing logic
        const url = new URL(request.url);
        if (url.pathname === "/api/hello") {
            return Response.json({ message: "Hello!" });
        }

return new Response("Not found", { status: 404 });
    },
} satisfies ExportedHandler<Env>;

Hono

import { Hono } from "hono";
import { agentsMiddleware } from "hono-agents";
export { CounterAgent } from "./agents/counter";

const app = new Hono();

// Add agents middleware - handles WebSocket upgrades and agent HTTP requests
app.use("*", agentsMiddleware());

// Your existing routes continue to work
app.get("/api/hello", (c) => c.json({ message: "Hello!" }));

export default app;
import { Hono } from "hono";
import { agentsMiddleware } from "hono-agents";
export { CounterAgent } from "./agents/counter";

const app = new Hono<{ Bindings: Env }>();

// Add agents middleware - handles WebSocket upgrades and agent HTTP requests
app.use("*", agentsMiddleware());

// Your existing routes continue to work
app.get("/api/hello", (c) => c.json({ message: "Hello!" }));

export default app;

With static assets

If you are serving static assets alongside agents, static assets are served first by default. Your Worker code only runs for paths that do not match a static asset:

import { routeAgentRequest } from "agents";
export { CounterAgent } from "./agents/counter";

export default {
    async fetch(request, env, ctx) {
        // Static assets are served automatically before this runs
        // This only handles non-asset requests

// Route to agents
        const agentResponse = await routeAgentRequest(request, env);
        if (agentResponse) return agentResponse;

return new Response("Not found", { status: 404 });
    },
};
import { routeAgentRequest } from "agents";
export { CounterAgent } from "./agents/counter";

export default {
    async fetch(request: Request, env: Env, ctx: ExecutionContext) {
        // Static assets are served automatically before this runs
        // This only handles non-asset requests

// Route to agents
        const agentResponse = await routeAgentRequest(request, env);
        if (agentResponse) return agentResponse;

return new Response("Not found", { status: 404 });
    },
} satisfies ExportedHandler<Env>;

Configure assets in the Wrangler configuration file:

{
    "assets": {
        "directory": "./public",
    },
}
[assets]
directory = "./public"

7. Generate TypeScript types

Do not hand-write your Env interface. Run wrangler types to generate a type definition file that matches your Wrangler configuration. This catches mismatches between your config and code at compile time instead of at deploy time.

Re-run wrangler types whenever you add or rename a binding.

npx wrangler types

This creates a type definition file with all your bindings typed, including your agent Durable Object namespaces. The Agent class defaults to using the generated Env type, so you do not need to pass it as a type parameter — extends Agent is sufficient unless you need to pass a second type parameter for state (for example, Agent<Env, CounterState>).

Refer to Configuration for more details on type generation.

8. Connect from the frontend

React

import { useState } from "react";
import { useAgent } from "agents/react";

function CounterWidget() {
    const [count, setCount] = useState(0);

const agent = useAgent({
        agent: "CounterAgent",
        onStateUpdate: (state) => setCount(state.count),
    });

return (
        <>
            {count}
            <button onClick={() => agent.stub.increment()}>+</button>
            <button onClick={() => agent.stub.decrement()}>-</button>
        </>
    );
}
import { useState } from "react";
import { useAgent } from "agents/react";
import type { CounterAgent, CounterState } from "./agents/counter";

function CounterWidget() {
    const [count, setCount] = useState(0);

const agent = useAgent<CounterAgent, CounterState>({
        agent: "CounterAgent",
        onStateUpdate: (state) => setCount(state.count),
    });

return (
        <>
            {count}
            <button onClick={() => agent.stub.increment()}>+</button>
            <button onClick={() => agent.stub.decrement()}>-</button>
        </>
    );
}

Key points:

  • useAgent connects to your agent via WebSocket
  • onStateUpdate fires whenever the agent's state changes
  • agent.stub.methodName() calls methods marked with @callable() on your agent

Vanilla JavaScript

import { AgentClient } from "agents/client";

const agent = new AgentClient({
    agent: "CounterAgent",
    name: "user-123", // Optional: unique instance name
    onStateUpdate: (state) => {
        document.getElementById("count").textContent = state.count;
    },
});

// Call methods
document.getElementById("increment").onclick = () => agent.call("increment");
import { AgentClient } from "agents/client";

const agent = new AgentClient({
    agent: "CounterAgent",
    name: "user-123", // Optional: unique instance name
    onStateUpdate: (state) => {
        document.getElementById("count").textContent = state.count;
    },
});

// Call methods
document.getElementById("increment").onclick = () => agent.call("increment");

How it works

When you clicked the button:

  1. Client called agent.stub.increment() over WebSocket
  2. Agent ran increment(), updated state with setState()
  3. State persisted to SQLite automatically
  4. Broadcast sent to all connected clients
  5. React updated via onStateUpdate
flowchart LR
    A["Browser<br/>(React)"] <-->|WebSocket| B["Agent<br/>(Counter)"]
    B --> C["SQLite<br/>(State)"]

Key concepts

Concept What it means
Agent instance Each unique name gets its own agent. CounterAgent:user-123 is separate from CounterAgent:user-456
Persistent state State survives restarts, deploys, and hibernation. It is stored in SQLite
Real-time sync All clients connected to the same agent receive state updates instantly
Hibernation When no clients are connected, the agent hibernates (no cost). It wakes on the next request