Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cloudflare/agents/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Agents provide built-in state management with automatic persistence to Durable Object storage and real-time synchronization to all connected WebSocket clients.
Setting State
setState()
Update the Agent’s state. Persists to storage and broadcasts to all connected clients.
class CounterAgent extends Agent<Env, { count: number }> {
initialState = { count: 0 };
@callable()
async increment() {
this.setState({ count: this.state.count + 1 });
}
}
Throws: Error if called from a readonly connection context
How setState() Works
- Validation - Calls
validateStateChange() hook (synchronous)
- Persistence - Saves state to Durable Object storage
- Broadcast - Sends state update to all connected clients
- Notification - Calls
onStateChanged() hook (async, non-blocking)
Reading State
state
Access the current state via the state property.
@callable()
async getCount() {
return this.state.count;
}
initialState
Define the initial state for new Agent instances.
class TodoAgent extends Agent<Env, { todos: Todo[] }> {
initialState = { todos: [] };
}
initialState is only used when the Agent is first created. If state already exists in storage, it takes precedence.
State Lifecycle Hooks
validateStateChange()
Called before state is persisted. Throw an error to reject the update. Must be synchronous.
source
Connection | 'server'
required
Source of the state update
validateStateChange(nextState: State, source: Connection | "server") {
// Only allow server to set admin fields
if (source !== "server" && nextState.isAdmin) {
throw new Error("Only server can set isAdmin");
}
// Validate state shape
if (nextState.count < 0) {
throw new Error("Count cannot be negative");
}
}
validateStateChange() must be synchronous. Use onStateChanged() for async operations.
onStateChanged()
Called after state has been persisted and broadcast. This is a notification hook—errors are routed to onError() and do not affect persistence.
state
State | undefined
required
The new state
source
Connection | 'server'
required
Source of the state update
async onStateChanged(state: State, source: Connection | "server") {
// Log state changes
console.log("State updated:", state);
// Trigger side effects
if (state.count >= 100) {
await this.queue("celebrate");
}
// Update external services
await fetch("https://api.example.com/notify", {
method: "POST",
body: JSON.stringify({ state })
});
}
Client-Side State Management
useAgent Hook
React hook for real-time state synchronization.
import { useAgent } from "agents/react";
function Counter() {
const [state, setState] = useState<{ count: number }>();
const agent = useAgent<{ count: number }>({
agent: "CounterAgent",
name: "default",
onStateUpdate: (newState) => setState(newState)
});
return (
<div>
<p>Count: {state?.count ?? 0}</p>
<button onClick={() => agent.call("increment")}>
Increment
</button>
</div>
);
}
See useAgent hook for full documentation.
AgentClient
Vanilla JavaScript client for state synchronization.
import { AgentClient } from "agents/client";
const client = new AgentClient<{ count: number }>({
host: "localhost:1999",
agent: "CounterAgent",
name: "default",
onStateUpdate: (state) => {
console.log("State updated:", state);
}
});
// Update state from client
client.setState({ count: 42 });
See AgentClient for full documentation.
Connection-Level State
Readonly Connections
Mark connections as readonly to prevent state updates.
class SecureAgent extends Agent<Env, State> {
shouldConnectionBeReadonly(connection: Connection, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
const isViewer = url.searchParams.get("role") === "viewer";
return isViewer;
}
}
connection.setState()
Connections can have their own isolated state (separate from Agent state).
async onConnect(connection: Connection, ctx: ConnectionContext) {
const userId = new URL(ctx.request.url).searchParams.get("user");
connection.setState({ userId, connectedAt: Date.now() });
}
Access connection state:
const { userId } = connection.state as { userId: string };
State Persistence
State is automatically persisted to Durable Object SQL storage:
CREATE TABLE cf_agents_state (
id TEXT PRIMARY KEY NOT NULL,
state TEXT
);
You can access the raw storage if needed:
const rows = this.sql<{ state: string }>`
SELECT state FROM cf_agents_state WHERE id = 'cf_state_row_id'
`;
Direct SQL access to state storage is not recommended. Use setState() and state instead.
State Broadcasting
When state changes, all connected clients receive an update message:
{
"type": "cf_agent_state",
"state": { "count": 42 }
}
You can suppress protocol messages for specific connections:
shouldSendProtocolMessages(connection: Connection, ctx: ConnectionContext) {
// Disable protocol messages for binary-only clients (e.g., MQTT)
const contentType = ctx.request.headers.get("content-type");
return contentType !== "application/octet-stream";
}
Best Practices
Atomic Updates
Always pass the complete state object to setState():
// ✅ Good
this.setState({ ...this.state, count: this.state.count + 1 });
// ❌ Bad - state is replaced entirely
this.setState({ count: this.state.count + 1 });
Validate Before Persist
Use validateStateChange() to enforce invariants:
validateStateChange(nextState: State) {
if (nextState.balance < 0) {
throw new Error("Balance cannot be negative");
}
}
Async Side Effects
Use onStateChanged() for async operations:
async onStateChanged(state: State) {
// Don't block state updates with slow operations
await this.env.KV.put("latest-state", JSON.stringify(state));
}
State Size
Keep state small (under 128KB recommended). For large data, use SQL or KV:
// ✅ Good - store ID in state, fetch full data as needed
this.setState({ currentDocumentId: "doc-123" });
// ❌ Bad - storing large document in state
this.setState({ currentDocument: { ...largeDocument } });