This guide shows how to add agents to an existing Cloudflare Workers project. If you're starting fresh, see Getting Started instead.
- An existing Cloudflare Workers project with
wrangler.jsonc - Node.js 18+
npm install agentsFor React applications, no additional packages are needed—React bindings are included.
For Hono applications:
npm install agents hono-agentsCreate a new file for your agent (e.g., src/agents/counter.ts):
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:
Key points:
namein bindings becomes the property onenv(e.g.,env.Counter)class_namemust match your exported class name exactlynew_sqlite_classesenables SQLite storage for state persistence- The
nodejs_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 {
// ...
};Choose the approach that matches your project structure:
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<{ 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're serving static assets alongside agents:
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;
// Fall back to static assets
return env.ASSETS.fetch(request);
}
};Make sure your wrangler.jsonc has the assets binding:
{
"assets": {
"binding": "ASSETS"
}
}Update your Env type to include the agent namespace. Create or update env.d.ts:
import type { Counter } from "./agents/counter";
interface Env {
// Your existing bindings
MY_KV: KVNamespace;
MY_DB: D1Database;
// Add agent bindings
Counter: DurableObjectNamespace<Counter>;
}import { useAgent } from "agents/react";
type CounterState = { count: number };
function CounterWidget() {
const agent = useAgent<CounterState>({
agent: "Counter"
});
return (
<div>
<span>{agent.state?.count ?? 0}</span>
<button onClick={() => agent.stub.increment()}>+</button>
<button onClick={() => agent.stub.decrement()}>-</button>
</div>
);
}import { AgentClient } from "agents/client";
const agent = new AgentClient({
agent: "Counter",
name: "user-123", // Optional: unique instance name
host: window.location.host,
onStateUpdate: (state) => {
// Update the DOM when state changes
document.getElementById("count").textContent = String(state.count);
}
});
// Call methods — agent.state is also readable directly
document.getElementById("increment").onclick = () => agent.call("increment");Add more agents by extending the configuration:
// src/agents/chat.ts
export class Chat extends Agent<Env, ChatState> {
// ...
}
// src/agents/scheduler.ts
export class Scheduler extends Agent<Env> {
// ...
}Update wrangler.jsonc:
{
"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"]
}
]
}Export all agents from your entry point:
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: 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}
});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.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_nameinwrangler.jsoncmust match the exported class name exactly - Check the route - Default route is
/agents/{agent-name}/{instance-name}
Add the migration to wrangler.jsonc:
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["YourAgentClass"]
}
]Ensure your routing passes the response through unchanged:
// ✅ Correct - return the response directly
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
// ❌ Wrong - don't wrap or modify the response
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return new Response(agentResponse.body); // Breaks WebSocketCheck that:
- You're using
this.setState(), not mutatingthis.statedirectly - The agent class is in
new_sqlite_classesin migrations - You're connecting to the same agent instance name
- State Management - Deep dive into agent state
- Scheduling - Background tasks and cron jobs
- Agent Class - Full lifecycle and methods
- Client SDK - Complete client API reference
{ "name": "my-existing-project", "main": "src/index.ts", "compatibility_date": "2025-01-01", "compatibility_flags": ["nodejs_compat"], // Required for agents // Add this section "durable_objects": { "bindings": [ { "name": "Counter", "class_name": "Counter" } ] }, // Add this section "migrations": [ { "tag": "v1", "new_sqlite_classes": ["Counter"] } ] }