| title | Add to existing project | ||
|---|---|---|---|
| pcx_content_type | how-to | ||
| sidebar |
|
import { PackageManagers, TypeScriptExample, WranglerConfig, LinkCard, } from "~/components";
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
For React applications, no additional packages are needed — React bindings are included.
For Hono applications:
Create a new file for your agent (for example, src/agents/counter.ts):
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:
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 {
// ...
} 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: 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<{ 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;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: 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",
},
}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";
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 methods
document.getElementById("increment").onclick = () => agent.call("increment");Add more agents by extending the configuration:
// src/agents/chat.ts
export class Chat extends Agent {
// ...
}
// src/agents/scheduler.ts
export 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"],
},
],
}Export all agents from your entry point:
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: 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}
});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: 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"],
},
],
}Ensure your routing passes the response unchanged:
// Correct - return the response directly
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
// Wrong - this breaks WebSocket connections
if (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.
{ "name": "my-existing-project", "main": "src/index.ts", "compatibility_date": "$today", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [ { "name": "CounterAgent", "class_name": "CounterAgent", }, ], }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["CounterAgent"], }, ], }