Quick start
Last updated Oct 12, 2026 Build AI agents that persist, think, and act. Agents run on Cloudflare's global network, maintain state across requests, and connect to clients in real-time via WebSockets.
What you will build: A counter agent with persistent state that syncs to a React frontend in real-time.
Time: \~10 minutes
Create a new project
npmyarnpnpm
npm create cloudflare@latest -- --template cloudflare/agents-starter
yarn create cloudflare --template cloudflare/agents-starter
pnpm create cloudflare@latest --template cloudflare/agents-starter
Then install dependencies and start the dev server:
cd agents-starter
npm install
npm run dev
This creates a project with:
src/server.ts— Your agent codesrc/client.tsx— React frontendwrangler.jsonc— Cloudflare configurationtsconfig.json— Extendsagents/tsconfigfor correct decorator and module settingsvite.config.ts— Includes theagents/viteplugin for decorator support
The starter template includes two important SDK integrations. If you are setting up a project manually, add both:
tsconfig.json — extends agents/tsconfig, which sets target: "ES2021" and other recommended options:
{
"extends": "agents/tsconfig"
}
vite.config.ts — includes the agents() plugin, which handles TC39 decorator transforms (required for @callable() in Vite 8):
import { cloudflare } from "@cloudflare/vite-plugin";
import react from "@vitejs/plugin-react";
import agents from "agents/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [agents(), react(), cloudflare()],
});
Open http://localhost:5173 ↗︎ to see your agent in action.
Your first agent
Build a simple counter agent from scratch. Replace src/server.ts:
import { Agent, routeAgentRequest, callable } from "agents";
// Define the state shape
// Create the agent
export class CounterAgent extends Agent {
// Initial state for new instances
initialState = { count: 0 };
// Methods marked with @callable can be called from the client
@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;
}
@callable()
reset() {
this.setState({ count: 0 });
}
}
// Route requests to agents
export default {
async fetch(request, env, ctx) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};
src/server.tsts
import { Agent, routeAgentRequest, callable } from "agents";
// Define the state shape
export type CounterState = {
count: number;
};
// Create the agent
export class CounterAgent extends Agent<Env, CounterState> {
// Initial state for new instances
initialState: CounterState = { count: 0 };
// Methods marked with @callable can be called from the client
@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;
}
@callable()
reset() {
this.setState({ count: 0 });
}
}
// Route requests to agents
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;
Update wrangler.jsonc to register the agent:
{
"name": "my-agent",
"main": "src/server.ts",
"upload_source_maps": true,
// Set this to today's date
"compatibility_date": "2026-10-27",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "CounterAgent",
"class_name": "CounterAgent",
},
],
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["CounterAgent"],
},
],
}
name = "my-agent"
main = "src/server.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"
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
Connect from React
Replace src/client.tsx:
src/client.tsxtsx
import "./styles.css";
import { createRoot } from "react-dom/client";
import { useState } from "react";
import { useAgent } from "agents/react";
import type { CounterAgent, CounterState } from "./server";
export default function App() {
const [count, setCount] = useState(0);
// Connect to the Counter agent
const agent = useAgent<CounterAgent, CounterState>({
agent: "CounterAgent",
onStateUpdate: (state) => setCount(state.count),
});
return (
<div style={{ padding: "2rem", fontFamily: "system-ui" }}>
<h1>Counter Agent</h1>
<p style={{ fontSize: "3rem" }}>{count}</p>
<div style={{ display: "flex", gap: "1rem" }}>
<button onClick={() => agent.stub.decrement()}>-</button>
<button onClick={() => agent.stub.reset()}>Reset</button>
<button onClick={() => agent.stub.increment()}>+</button>
</div>
</div>
);
}
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
Key points:
useAgentconnects to your agent via WebSocketonStateUpdatefires whenever the agent's state changesagent.stub.methodName()calls methods marked with@callable()on your agent
How it works
When you clicked the button:
- Client called
agent.stub.increment()over WebSocket - Agent ran
increment(), updated state withsetState() - State persisted to SQLite automatically
- Broadcast sent to all connected clients
- React updated via
onStateUpdate
flowchart LR
A["Browser<br/>(React)"] <-->|WebSocket| B["Agent<br/>(Counter)"]
B --> C["SQLite<br/>(State)"]
Key concepts
| Concept | What it means |
|---|---|
| Agent instance | Each unique name gets its own agent. CounterAgent:user-123 is separate from CounterAgent:user-456 |
| Persistent state | State survives restarts, deploys, and hibernation. It is stored in SQLite |
| Real-time sync | All clients connected to the same agent receive state updates instantly |
| Hibernation | When no clients are connected, the agent hibernates (no cost). It wakes on the next request |
Connect from vanilla JavaScript
If you are not using React:
import { AgentClient } from "agents/client";
const agent = new AgentClient({
agent: "CounterAgent",
name: "my-counter", // optional, defaults to "default"
onStateUpdate: (state) => {
console.log("New count:", state.count);
},
});
// Call methods
await agent.call("increment");
await agent.call("reset");
import { AgentClient } from "agents/client";
const agent = new AgentClient({
agent: "CounterAgent",
name: "my-counter", // optional, defaults to "default"
onStateUpdate: (state) => {
console.log("New count:", state.count);
},
});
// Call methods
await agent.call("increment");
await agent.call("reset");
Deploy to Cloudflare
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:
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>;
Custom agent path prefix
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.
Accessing agents from server code
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 });
}
// ...
},
};
