Skip to content
Cloudflare Docs

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.

Prerequisites

  • An existing Cloudflare Workers project with a Wrangler configuration file
  • Node.js 18 or newer

1. Install the package

Terminal window
npm i agents

For React applications, no additional packages are needed — React bindings are included.

For Hono applications:

Terminal window
npm i agents hono-agents

2. Create an Agent

Create a new file for your agent (for example, src/agents/counter.ts):

JavaScript
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;
}
}

3. Update Wrangler configuration

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"],
},
],
}

Key points:

  • name in bindings becomes the property on env (for example, env.Counter)
  • class_name must exactly match your exported class name
  • new_sqlite_classes enables SQLite storage for state persistence
  • nodejs_compat flag is required for the agents package

4. Export the Agent class

Your agent class must be exported from your main entry point. Update your src/index.ts:

JavaScript
// Export the agent class (required for Durable Objects)
export { Counter } from "./agents/counter";
// Your existing exports...
export default {
// ...
};

5. Wire up routing

Choose the approach that matches your project structure:

Plain Workers (fetch handler)

JavaScript
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 });
},
};

Hono

JavaScript
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 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:

JavaScript
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 });
},
};

Configure assets in the Wrangler configuration file:

{
"assets": {
"directory": "./public",
},
}

6. Add TypeScript types

You can generate types automatically from your Wrangler configuration file:

Terminal window
npx wrangler types env.d.ts

This creates an env.d.ts file with all your bindings typed. Alternatively, you can manually update your Env type to include the agent namespace:

TypeScript
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.

7. Connect from the frontend

React

JavaScript
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>
</>
);
}

Vanilla JavaScript

JavaScript
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 methods
document.getElementById("increment").onclick = () => agent.call("increment");

Adding multiple agents

Add more agents by extending the configuration:

JavaScript
// 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": "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:

JavaScript
export { Counter } from "./agents/counter";
export { Chat } from "./agents/chat";
export { Scheduler } from "./agents/scheduler";

Common integration patterns

Agents behind authentication

Check auth before routing to agents:

JavaScript
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
},
};

Custom agent path prefix

By default, agents are routed at /agents/{agent-name}/{instance-name}. You can customize this:

JavaScript
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.

Accessing agents from server code

You can interact with agents directly from your Worker code:

JavaScript
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 });
}
// ...
},
};

Troubleshooting

Agent not found, or 404 errors

  1. Check the export - Agent class must be exported from your main entry point.
  2. Check the binding - class_name in the Wrangler configuration file must exactly match the exported class name.
  3. Check the route - Default route is /agents/{agent-name}/{instance-name}.

No such Durable Object class error

Add the migration to the Wrangler configuration file:

{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["YourAgentClass"],
},
],
}

WebSocket connection fails

Ensure your routing passes the response unchanged:

JavaScript
// 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);

State not persisting

Check that:

  1. You are using this.setState(), not mutating this.state directly.
  2. The agent class is in new_sqlite_classes in migrations.
  3. You are connecting to the same agent instance name.

Next steps