Agents provide built-in state management with automatic persistence and real-time synchronization across all connected clients.
Agent state is:
- Persistent - Automatically saved to SQLite, survives restarts and hibernation
- Synchronized - Changes broadcast to all connected WebSocket clients instantly
- Bidirectional - Both server and clients can update state
- Type-safe - Full TypeScript support with generics
import { Agent } from "agents";
type GameState = {
players: string[];
score: number;
status: "waiting" | "playing" | "finished";
};
export class GameAgent extends Agent<Env, GameState> {
// Default state for new agents
initialState: GameState = {
players: [],
score: 0,
status: "waiting"
};
// React to state changes
onStateChanged(state: GameState, source: Connection | "server") {
if (source !== "server" && state.players.length >= 2) {
// Client added a player, start the game
this.setState({ ...state, status: "playing" });
}
}
addPlayer(name: string) {
this.setState({
...this.state,
players: [...this.state.players, name]
});
}
}Use the initialState property to define default values for new agent instances:
type State = {
messages: Message[];
settings: UserSettings;
lastActive: string | null;
};
export class ChatAgent extends Agent<Env, State> {
initialState: State = {
messages: [],
settings: { theme: "dark", notifications: true },
lastActive: null
};
}The second generic parameter to Agent<Env, State> defines your state type:
// State is fully typed
export class MyAgent extends Agent<Env, MyState> {
initialState: MyState = { count: 0 };
increment() {
// TypeScript knows this.state is MyState
this.setState({ count: this.state.count + 1 });
}
}Initial state is applied lazily on first access, not on every wake:
- New agent -
initialStateis used and persisted - Existing agent - Persisted state is loaded from SQLite
- No
initialStatedefined -this.stateisundefined
async onStart() {
// Safe to access - returns initialState if new, or persisted state
console.log("Current count:", this.state.count);
}Access the current state via the this.state getter:
async onRequest(request: Request) {
// Read current state
const { players, status } = this.state;
if (status === "waiting" && players.length < 2) {
return new Response("Waiting for players...");
}
return new Response(JSON.stringify(this.state));
}If you don't define initialState, this.state returns undefined:
export class MinimalAgent extends Agent<Env> {
// No initialState defined
async onConnect(connection: Connection) {
if (!this.state) {
// First time - initialize state
this.setState({ initialized: true });
}
}
}Use setState() to update state. This:
- Saves to SQLite (persistent)
- Broadcasts to all connected clients
- Triggers
onStateChanged()(after broadcast; best-effort)
// Replace entire state
this.setState({
players: ["Alice", "Bob"],
score: 0,
status: "playing"
});
// Update specific fields (spread existing state)
this.setState({
...this.state,
score: this.state.score + 10
});State is stored as JSON, so it must be serializable:
// Good - plain objects, arrays, primitives
this.setState({
items: ["a", "b", "c"],
count: 42,
active: true,
metadata: { key: "value" }
});
// Bad - functions, classes, circular references
this.setState({
callback: () => {}, // Functions don't serialize
date: new Date(), // Becomes string, loses methods
self: this // Circular reference
});
// For dates, use ISO strings
this.setState({
createdAt: new Date().toISOString()
});Override onStateChanged() to react when state changes (notifications/side-effects):
onStateChanged(state: GameState, source: Connection | "server") {
console.log("State updated:", state);
console.log("Updated by:", source === "server" ? "server" : source.id);
}If you want to validate or reject state updates, override validateStateChange():
- Runs before persistence and broadcast
- Must be synchronous
- Throwing aborts the update
validateStateChange(nextState: GameState, source: Connection | "server") {
// Example: reject negative scores
if (nextState.score < 0) {
throw new Error("score cannot be negative");
}
}
onStateChanged()is not intended for validation; it is a notification hook and should not block broadcasts.Migration note:
onStateChangedreplaces the deprecatedonStateUpdate(server-side hook). If you're usingonStateUpdateon your agent class, rename it toonStateChanged— the signature and behavior are identical. A console warning will fire once per class until you rename it.
The source tells you who triggered the update:
| Value | Meaning |
|---|---|
"server" |
Agent called setState() |
Connection |
A client pushed state via WebSocket |
This is useful for:
- Avoiding infinite loops (don't react to your own updates)
- Validating client input
- Triggering side effects only on client actions
onStateChanged(state: State, source: Connection | "server") {
// Ignore server-initiated updates
if (source === "server") return;
// A client updated state - validate and process
const connection = source;
console.log(`Client ${connection.id} updated state`);
// Maybe trigger something based on the change
if (state.status === "submitted") {
this.processSubmission(state);
}
}onStateChanged(state: State, source: Connection | "server") {
if (source === "server") return;
// Client added a message
const lastMessage = state.messages[state.messages.length - 1];
if (lastMessage && !lastMessage.processed) {
// Process and update
this.setState({
...state,
messages: state.messages.map(m =>
m.id === lastMessage.id ? { ...m, processed: true } : m
)
});
}
}State synchronizes automatically with connected clients. Both useAgent and AgentClient expose a state property that tracks the current agent state. See Client SDK for full details.
import { useAgent } from "agents/react";
function GameUI() {
const agent = useAgent({
agent: "game-agent",
name: "room-123"
});
// Read state directly — reactive, triggers re-render on change
// Push state to agent with spread for partial updates
const addPlayer = (name: string) => {
agent.setState({
...agent.state,
players: [...(agent.state?.players ?? []), name]
});
};
return <div>Players: {agent.state?.players.join(", ")}</div>;
}import { AgentClient } from "agents/client";
const client = new AgentClient({
agent: "game-agent",
name: "room-123",
host: "your-worker.workers.dev"
});
await client.ready;
// Read state directly
console.log("Score:", client.state?.score);
// Push state update with spread for partial updates
client.setState({ ...client.state, score: 100 });┌─────────────────────────────────────────────────────────────┐
│ Agent │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ this.state │ │
│ │ (persisted in SQLite) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▲ │ │
│ │ setState() │ broadcast │
│ │ ▼ │
└───────────┼──────────────────────────────┼──────────────────┘
│ │
│ │ WebSocket
│ │
┌───────────┴──────────────────────────────┴───────────────────┐
│ Clients │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client 1 │ │ Client 2 │ │ Client 3 │ │
│ │ state │ │ state │ │ state │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Any client can call setState() to push updates │
└──────────────────────────────────────────────────────────────┘
When using Workflows, you can update agent state from workflow steps:
// In your workflow
async run(event: AgentWorkflowEvent<Params>, step: AgentWorkflowStep) {
// Replace entire state
await step.updateAgentState({ status: "processing", progress: 0 });
// Merge partial updates (preserves other fields)
await step.mergeAgentState({ progress: 50 });
// Reset to initialState
await step.resetAgentState();
return result;
}These are durable operations - they persist even if the workflow retries.
State is broadcast to all clients on every change. For large data:
// Bad - storing large arrays in state
initialState = {
allMessages: [] // Could grow to thousands of items
};
// Good - store in SQL, keep state light
initialState = {
messageCount: 0,
lastMessageId: null
};
// Query SQL for full data
async getMessages(limit = 50) {
return this.sql`SELECT * FROM messages ORDER BY created_at DESC LIMIT ${limit}`;
}For responsive UIs, update client state immediately:
// Client-side
function sendMessage(text: string) {
const optimisticMessage = {
id: crypto.randomUUID(),
text,
pending: true
};
// Update immediately — agent.state updates optimistically
agent.setState({
...agent.state,
messages: [...(agent.state?.messages ?? []), optimisticMessage]
});
// Server will confirm/update
}
// Server-side
onStateChanged(state: State, source: Connection | "server") {
if (source === "server") return;
const pendingMessages = state.messages.filter(m => m.pending);
for (const msg of pendingMessages) {
// Validate and confirm
this.setState({
...state,
messages: state.messages.map(m =>
m.id === msg.id ? { ...m, pending: false, timestamp: Date.now() } : m
)
});
}
}| Use State For | Use SQL For |
|---|---|
| UI state (loading, selected items) | Historical data |
| Real-time counters | Large collections |
| Active session data | Relationships |
| Configuration | Queryable data |
export class ChatAgent extends Agent<Env, State> {
// State: current UI state
initialState = {
typing: [],
unreadCount: 0,
activeUsers: []
};
// SQL: message history
async getMessages(limit = 100) {
return this.sql`
SELECT * FROM messages
ORDER BY created_at DESC
LIMIT ${limit}
`;
}
async saveMessage(message: Message) {
this.sql`
INSERT INTO messages (id, text, user_id, created_at)
VALUES (${message.id}, ${message.text}, ${message.userId}, ${Date.now()})
`;
// Update state for real-time UI
this.setState({
...this.state,
unreadCount: this.state.unreadCount + 1
});
}
}Be careful not to trigger state updates in response to your own updates:
// Bad - infinite loop
onStateChanged(state: State) {
this.setState({ ...state, lastUpdated: Date.now() });
}
// Good - check source
onStateChanged(state: State, source: Connection | "server") {
if (source === "server") return; // Don't react to own updates
this.setState({ ...state, lastUpdated: Date.now() });
}| Property | Type | Description |
|---|---|---|
state |
State |
Current state (getter) |
initialState |
State |
Default state for new agents |
| Method | Signature | Description |
|---|---|---|
setState |
(state: State) => void |
Update state, persist, and broadcast |
onStateChanged |
(state: State, source: Connection | "server") => void |
Called after state is persisted and broadcast |
| Method | Description |
|---|---|
step.updateAgentState(state) |
Replace agent state from workflow |
step.mergeAgentState(partial) |
Merge partial state from workflow |
step.resetAgentState() |
Reset to initialState from workflow |
- Readonly Connections - Restrict which connections can update state
- Client SDK - Full client-side state sync documentation
- Workflows - Durable state updates from workflows
- SQL API - When to use SQL instead of state