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:
namein bindings becomes the property onenv(for example,env.CounterAgent)class_namemust exactly match your exported class namenew_sqlite_classesenables SQLite storage for state persistencenodejs_compatflag 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:
useAgentconnects to your agent via WebSocketonStateUpdatefires whenever the agent's state changesagent.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:
- Client called
agent.stub.increment()over WebSocket - Agent ran
increment(), updated state withsetState() - State persisted to SQLite automatically
- Broadcast sent to all connected clients
- 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 |
