Skip to content

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

npm i agents

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

For Hono applications:

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

3. Update Wrangler configuration

Add the Durable Object binding and migration:

JSONC
{
"name": "my-existing-project",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-06-30",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "CounterAgent",
"class_name": "CounterAgent",
},
],
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["CounterAgent"],
},
],
}

Key points:

  • name in bindings becomes the property on env (for example, env.CounterAgent)
  • 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. Configure TypeScript and Vite

If you use @callable() decorators (as in the example above), you need two build configurations.

tsconfig.json — extend agents/tsconfig (or set "target": "ES2021" manually):

{
"extends": "agents/tsconfig"
}

If you have an existing tsconfig.json with custom settings, you can extend and override:

{
"extends": "agents/tsconfig",
"compilerOptions": {
"paths": { "~/*": ["./src/*"] }
}
}

vite.config.ts — add the agents() plugin (handles TC39 decorator transforms for Vite 8):

JavaScript
import agents from "agents/vite";
export default defineConfig({
plugins: [
agents(),
// ... your existing plugins
],
});

If your project does not use Vite, the tsconfig.json change alone is sufficient — your bundler must support TC39 decorators (stage 3, version 2023-11).

For more details, refer to the TypeScript configuration and Vite configuration reference.

5. 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 { CounterAgent } from "./agents/counter";
// Your existing exports...
export default {
// ...
};

6. Wire up routing

Choose the approach that matches your project structure:

Plain Workers (fetch handler)

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

Hono

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

Configure assets in the Wrangler configuration file:

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

7. Generate TypeScript types

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.

Terminal window
npx wrangler types

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

8. 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: "CounterAgent",
onStateUpdate: (state) => setCount(state.count),
});
return (
<>
{count}
<button onClick={() => agent.stub.increment()}>+</button>
<button onClick={() => agent.stub.decrement()}>-</button>
</>
);
}

Key points:

  • useAgent connects to your agent via WebSocket
  • onStateUpdate fires whenever the agent's state changes
  • agent.stub.methodName() calls methods marked with @callable() on your agent

Vanilla JavaScript

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

How it works

When you clicked the button:

  1. Client called agent.stub.increment() over WebSocket
  2. Agent ran increment(), updated state with setState()
  3. State persisted to SQLite automatically
  4. Broadcast sent to all connected clients
  5. React updated via onStateUpdate
flowchart LR
    A["Browser<br/>(React)"] <-->|WebSocket| B["Agent<br/>(Counter)"]
    B --> C["SQLite<br/>(State)"]

Key concepts

ConceptWhat it means
Agent instanceEach unique name gets its own agent. CounterAgent:user-123 is separate from CounterAgent:user-456
Persistent stateState survives restarts, deploys, and hibernation. It is stored in SQLite
Real-time syncAll clients connected to the same agent receive state updates instantly
HibernationWhen no clients are connected, the agent hibernates (no cost). It wakes on the next request

Deploy to Cloudflare

Terminal window
npm run deploy

Your agent is now live on Cloudflare's global network, running close to your users.

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.CounterAgent, "shared-counter");
const newCount = await counter.increment();
return Response.json({ count: newCount });
}
// ...
},
};

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:

JSONC
{
"$schema": "./node_modules/wrangler/config-schema.json",
"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:

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

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}'}. Agent name in client matches the class name (case-insensitive).

No such Durable Object class error

Add the migration to the Wrangler configuration file:

JSONC
{
"$schema": "./node_modules/wrangler/config-schema.json",
"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 calling 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.
  4. The onStateUpdate callback is wired up in your client.
  5. WebSocket connection is established (check browser dev tools).

"Method X is not callable" errors

Make sure your methods are decorated with @callable():

JavaScript
import { Agent, callable } from "agents";
export class MyAgent extends Agent {
@callable()
increment() {
// ...
}
}

Type errors with agent.stub

Add the agent and state type parameters:

JavaScript
import { useAgent } from "agents/react";
// Pass the agent and state types to useAgent
const agent = useAgent({
agent: "CounterAgent",
onStateUpdate: (state) => setCount(state.count),
});
// Now agent.stub is fully typed
agent.stub.increment();

SyntaxError: Invalid or unexpected token with @callable()

If your dev server fails with SyntaxError: Invalid or unexpected token, set "target": "ES2021" in your tsconfig.json. This ensures that Vite's esbuild transpiler downlevels TC39 decorators instead of passing them through as native syntax.

{
"compilerOptions": {
"target": "ES2021"
}
}

Next steps

Now that you have a working agent, explore these topics:

Common next steps

Learn how toRefer to
Add AI/LLM capabilitiesUsing AI models
Expose tools via MCPMCP servers
Run background tasksSchedule tasks
Handle emailsEmail routing
Use Cloudflare WorkflowsRun Workflows

Explore more