Add to existing project
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.
- An existing Cloudflare Workers project with a Wrangler configuration file
- Node.js 18 or newer
npm i agentsyarn add agentspnpm add agentsFor React applications, no additional packages are needed — React bindings are included.
For Hono applications:
npm i agents hono-agentsyarn add agents hono-agentspnpm add agents hono-agentsCreate a new file for your agent (for example, src/agents/counter.ts):
import { Agent } from "agents";
export class Counter extends Agent { initialState = { count: 0 };
increment() { this.setState({ count: this.state.count + 1 }); return this.state.count; }
decrement() { this.setState({ count: this.state.count - 1 }); return this.state.count; }}import { Agent } from "agents";
type CounterState = { count: number;};
export class Counter extends Agent<Env, CounterState> { initialState: CounterState = { count: 0 };
increment() { this.setState({ count: this.state.count + 1 }); return this.state.count; }
decrement() { this.setState({ count: this.state.count - 1 }); return this.state.count; }}Add the Durable Object binding and migration:
{ "name": "my-existing-project", "main": "src/index.ts", "compatibility_date": "2025-01-01", "compatibility_flags": ["nodejs_compat"],
"durable_objects": { "bindings": [ { "name": "Counter", "class_name": "Counter", }, ], },
"migrations": [ { "tag": "v1", "new_sqlite_classes": ["Counter"], }, ],}name = "my-existing-project"main = "src/index.ts"compatibility_date = "2025-01-01"compatibility_flags = [ "nodejs_compat" ]
[[durable_objects.bindings]]name = "Counter"class_name = "Counter"
[[migrations]]tag = "v1"new_sqlite_classes = [ "Counter" ]Key points:
namein bindings becomes the property onenv(for example,env.Counter)class_namemust exactly match your exported class namenew_sqlite_classesenables SQLite storage for state persistencenodejs_compatflag is required for the agents package
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 { Counter } from "./agents/counter";
// Your existing exports...export default { // ...};// Export the agent class (required for Durable Objects)export { Counter } from "./agents/counter";
// Your existing exports...export default { // ...};Choose the approach that matches your project structure:
import { routeAgentRequest } from "agents";export { Counter } 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 { Counter } 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 }); },};import { Hono } from "hono";import { agentsMiddleware } from "hono-agents";export { Counter } from "./agents/counter";
const app = new Hono();
// Add agents middleware - handles WebSocket upgrades and agent HTTP requestsapp.use("*", agentsMiddleware());
// Your existing routes continue to workapp.get("/api/hello", (c) => c.json({ message: "Hello!" }));
export default app;import { Hono } from "hono";import { agentsMiddleware } from "hono-agents";export { Counter } from "./agents/counter";
const app = new Hono<{ Bindings: Env }>();
// Add agents middleware - handles WebSocket upgrades and agent HTTP requestsapp.use("*", agentsMiddleware());
// Your existing routes continue to workapp.get("/api/hello", (c) => c.json({ message: "Hello!" }));
export default app;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 { Counter } 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 { Counter } 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 }); },};Configure assets in the Wrangler configuration file:
{ "assets": { "directory": "./public", },}[assets]directory = "./public"You can generate types automatically from your Wrangler configuration file:
npx wrangler types env.d.tsThis creates an env.d.ts file with all your bindings typed. Alternatively, you can manually update your Env type to include the agent namespace:
import type { Counter } from "./agents/counter";
interface Env { // Your existing bindings MY_KV: KVNamespace; MY_DB: D1Database;
// Add agent bindings Counter: DurableObjectNamespace<Counter>;}Refer to Configuration for more details on type generation.
import { useState } from "react";import { useAgent } from "agents/react";
function CounterWidget() { const [count, setCount] = useState(0);
const agent = useAgent({ agent: "Counter", 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";
type CounterState = { count: number };
function CounterWidget() { const [count, setCount] = useState(0);
const agent = useAgent({ agent: "Counter", onStateUpdate: (state) => setCount(state.count), });
return ( <> {count} <button onClick={() => agent.stub.increment()}>+</button> <button onClick={() => agent.stub.decrement()}>-</button> </> );}import { AgentClient } from "agents/client";
const agent = new AgentClient({ agent: "Counter", name: "user-123", // Optional: unique instance name onStateUpdate: (state) => { document.getElementById("count").textContent = state.count; },});
// Call methodsdocument.getElementById("increment").onclick = () => agent.call("increment");import { AgentClient } from "agents/client";
const agent = new AgentClient({ agent: "Counter", name: "user-123", // Optional: unique instance name onStateUpdate: (state) => { document.getElementById("count").textContent = state.count; },});
// Call methodsdocument.getElementById("increment").onclick = () => agent.call("increment");Add more agents by extending the configuration:
// src/agents/chat.tsexport class Chat extends Agent { // ...}
// src/agents/scheduler.tsexport class Scheduler extends Agent { // ...}// src/agents/chat.tsexport class Chat extends Agent { // ...}
// src/agents/scheduler.tsexport class Scheduler extends Agent { // ...}Update the Wrangler configuration file:
{ "durable_objects": { "bindings": [ { "name": "Counter", "class_name": "Counter" }, { "name": "Chat", "class_name": "Chat" }, { "name": "Scheduler", "class_name": "Scheduler" }, ], }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["Counter", "Chat", "Scheduler"], }, ],}[[durable_objects.bindings]]name = "Counter"class_name = "Counter"
[[durable_objects.bindings]]name = "Chat"class_name = "Chat"
[[durable_objects.bindings]]name = "Scheduler"class_name = "Scheduler"
[[migrations]]tag = "v1"new_sqlite_classes = [ "Counter", "Chat", "Scheduler" ]Export all agents from your entry point:
export { Counter } from "./agents/counter";export { Chat } from "./agents/chat";export { Scheduler } from "./agents/scheduler";export { Counter } from "./agents/counter";export { Chat } from "./agents/chat";export { Scheduler } from "./agents/scheduler";Check auth before routing to agents:
export default { async fetch(request, env) { // Check auth for agent routes if (request.url.includes("/agents/")) { const authResult = await checkAuth(request, env); if (!authResult.valid) { return new Response("Unauthorized", { status: 401 }); } }
const agentResponse = await routeAgentRequest(request, env); if (agentResponse) return agentResponse;
// ... rest of routing },};export default { async fetch(request: Request, env: Env) { // Check auth for agent routes if (request.url.includes("/agents/")) { const authResult = await checkAuth(request, env); if (!authResult.valid) { return new Response("Unauthorized", { status: 401 }); } }
const agentResponse = await routeAgentRequest(request, env); if (agentResponse) return agentResponse;
// ... rest of routing },};By default, agents are routed at /agents/{agent-name}/{instance-name}. You can customize this:
import { routeAgentRequest } from "agents";
const agentResponse = await routeAgentRequest(request, env, { prefix: "/api/agents", // Now routes at /api/agents/{agent-name}/{instance-name}});import { routeAgentRequest } from "agents";
const agentResponse = await routeAgentRequest(request, env, { prefix: "/api/agents", // Now routes at /api/agents/{agent-name}/{instance-name}});Refer to Routing for more options including CORS, custom instance naming, and location hints.
You can interact with agents directly from your Worker code:
import { getAgentByName } from "agents";
export default { async fetch(request, env) { if (request.url.endsWith("/api/increment")) { // Get a specific agent instance const counter = await getAgentByName(env.Counter, "shared-counter"); const newCount = await counter.increment(); return Response.json({ count: newCount }); } // ... },};import { getAgentByName } from "agents";
export default { async fetch(request: Request, env: Env) { if (request.url.endsWith("/api/increment")) { // Get a specific agent instance const counter = await getAgentByName(env.Counter, "shared-counter"); const newCount = await counter.increment(); return Response.json({ count: newCount }); } // ... },};- Check the export - Agent class must be exported from your main entry point.
- Check the binding -
class_namein the Wrangler configuration file must exactly match the exported class name. - Check the route - Default route is
/agents/{agent-name}/{instance-name}.
Add the migration to the Wrangler configuration file:
{ "migrations": [ { "tag": "v1", "new_sqlite_classes": ["YourAgentClass"], }, ],}[[migrations]]tag = "v1"new_sqlite_classes = [ "YourAgentClass" ]Ensure your routing passes the response unchanged:
// Correct - return the response directlyconst agentResponse = await routeAgentRequest(request, env);if (agentResponse) return agentResponse;
// Wrong - this breaks WebSocket connectionsif (agentResponse) return new Response(agentResponse.body);// Correct - return the response directlyconst agentResponse = await routeAgentRequest(request, env);if (agentResponse) return agentResponse;
// Wrong - this breaks WebSocket connectionsif (agentResponse) return new Response(agentResponse.body);Check that:
- You are using
this.setState(), not mutatingthis.statedirectly. - The agent class is in
new_sqlite_classesin migrations. - You are connecting to the same agent instance name.
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2026 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-