Quick start
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
npm create cloudflare@latest -- -- --template cloudflare/agents-starteryarn create cloudflare -- --template cloudflare/agents-starterpnpm create cloudflare@latest -- --template cloudflare/agents-starterThen install dependencies and start the dev server:
cd my-agentnpm installnpm run devThis creates a project with:
src/server.ts— Your agent codesrc/client.tsx— React frontendwrangler.jsonc— Cloudflare configuration
Open http://localhost:5173 ↗ to see your agent in action.
Build a simple counter agent from scratch. Replace src/server.ts:
import { Agent, routeAgentRequest, callable } from "agents";
// Define the state shape// Create the agentexport class Counter 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 agentsexport default { async fetch(request, env, ctx) { return ( (await routeAgentRequest(request, env)) ?? new Response("Not found", { status: 404 }) ); },};import { Agent, routeAgentRequest, callable } from "agents";
// Define the state shapetype CounterState = { count: number;};
// Create the agentexport class Counter 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 agentsexport default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { return ( (await routeAgentRequest(request, env)) ?? new Response("Not found", { status: 404 }) ); },};Update wrangler.jsonc to register the agent:
{ "name": "my-agent", "main": "src/server.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"], }, ],}name = "my-agent"main = "src/server.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" ]Replace src/client.tsx:
import { useState } from "react";import { useAgent } from "agents/react";import type { Counter } from "./server";
// Match your agent's state typetype CounterState = { count: number;};
export default function App() { const [count, setCount] = useState(0);
// Connect to the Counter agent const agent = useAgent<Counter, CounterState>({ agent: "Counter", 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> );}Key points:
useAgentconnects to your agent via WebSocketonStateUpdatefires whenever the agent's state changesagent.stub.methodName()calls methods marked with@callable()on your agent
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)"]
| Concept | What it means |
|---|---|
| Agent instance | Each unique name gets its own agent. Counter:user-123 is separate from Counter: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 |
If you are not using React:
import { AgentClient } from "agents/client";
const agent = new AgentClient({ agent: "Counter", name: "my-counter", // optional, defaults to "default" onStateUpdate: (state) => { console.log("New count:", state.count); },});
// Call methodsawait agent.call("increment");await agent.call("reset");import { AgentClient } from "agents/client";
const agent = new AgentClient({ agent: "Counter", name: "my-counter", // optional, defaults to "default" onStateUpdate: (state) => { console.log("New count:", state.count); },});
// Call methodsawait agent.call("increment");await agent.call("reset");npm run deployYour agent is now live on Cloudflare's global network, running close to your users.
Make sure:
- Agent class is exported from your server file
wrangler.jsonchas the binding and migration- Agent name in client matches the class name (case-insensitive)
Check that:
- You are calling
this.setState(), not mutatingthis.statedirectly - The
onStateUpdatecallback is wired up in your client - WebSocket connection is established (check browser dev tools)
Make sure your methods are decorated with @callable():
import { Agent, callable } from "agents";
export class MyAgent extends Agent { @callable() increment() { // ... }}import { Agent, callable } from "agents";
export class MyAgent extends Agent { @callable() increment() { // ... }}Add the agent and state type parameters:
import { useAgent } from "agents/react";// Pass the agent and state types to useAgentconst agent = useAgent({ agent: "Counter", onStateUpdate: (state) => setCount(state.count),});
// Now agent.stub is fully typedagent.stub.increment();import { useAgent } from "agents/react";import type { Counter } from "./server";
type CounterState = { count: number };
// Pass the agent and state types to useAgentconst agent = useAgent<Counter, CounterState>({ agent: "Counter", onStateUpdate: (state) => setCount(state.count),});
// Now agent.stub is fully typedagent.stub.increment();Now that you have a working agent, explore these topics:
| Learn how to | Refer to |
|---|---|
| Add AI/LLM capabilities | Using AI models |
| Expose tools via MCP | MCP servers |
| Run background tasks | Schedule tasks |
| Handle emails | Email routing |
| Use Cloudflare Workflows | Run Workflows |
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2026 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-