Skip to main content

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.
state
State
required
The new state to set
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

  1. Validation - Calls validateStateChange() hook (synchronous)
  2. Persistence - Saves state to Durable Object storage
  3. Broadcast - Sends state update to all connected clients
  4. 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.
nextState
State
required
The proposed new state
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 } });

Build docs developers (and LLMs) love