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, 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; }}Add the Durable Object binding and migration:
{ "name": "my-existing-project", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-02-26", "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"# Set this to today's datecompatibility_date = "2026-02-26"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
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>;Choose the approach that matches your project structure:
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>;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 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 { CounterAgent } 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 { 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"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 typesThis 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.
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> </> );}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 methodsdocument.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 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": "CounterAgent", "class_name": "CounterAgent" }, { "name": "Chat", "class_name": "Chat" }, { "name": "Scheduler", "class_name": "Scheduler" }, ], }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["CounterAgent", "Chat", "Scheduler"], }, ],}[[durable_objects.bindings]]name = "CounterAgent"class_name = "CounterAgent"
[[durable_objects.bindings]]name = "Chat"class_name = "Chat"
[[durable_objects.bindings]]name = "Scheduler"class_name = "Scheduler"
[[migrations]]tag = "v1"new_sqlite_classes = [ "CounterAgent", "Chat", "Scheduler" ]Export all agents from your entry point:
export { CounterAgent } from "./agents/counter";export { Chat } from "./agents/chat";export { Scheduler } from "./agents/scheduler";export { CounterAgent } 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 },} satisfies ExportedHandler<Env>;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.CounterAgent, "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.CounterAgent, "shared-counter"); const newCount = await counter.increment(); return Response.json({ count: newCount }); } // ... },} satisfies ExportedHandler<Env>;- 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.