Store and sync state
Last updated Jun 3, 2026 Agents provide built-in state management with automatic persistence and real-time synchronization across all connected clients.
Overview
State within an Agent is:
- Persistent - Automatically saves to SQLite, survives restarts and hibernation
- Synchronized - Changes are broadcast to all connected WebSocket clients instantly
- Bidirectional - Both server and clients can update state
- Type-safe - Full TypeScript support with generics
- Immediately consistent - Read your own writes
- Thread-safe - Safe for concurrent updates
- Fast - State is colocated wherever the Agent is running
Agent state is stored in a SQL database embedded within each individual Agent instance. You can interact with it using the higher-level this.setState API (recommended), which allows you to sync state and trigger events on state changes, or by directly querying the database with this.sql.
State vs Props
State is persistent data that survives restarts and syncs across clients. Props are one-time initialization arguments passed when an agent is instantiated - use props for configuration that does not need to persist.
import { Agent } from "agents";
export class GameAgent extends Agent {
// Default state for new agents
initialState = {
players: [],
score: 0,
status: "waiting",
};
// React to state changes
onStateChanged(state, source) {
if (source !== "server" && state.players.length >= 2) {
// Client added a player, start the game
this.setState({ ...state, status: "playing" });
}
}
addPlayer(name) {
this.setState({
...this.state,
players: [...this.state.players, name],
});
}
}
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],
});
}
}
Defining initial state
Use the initialState property to define default values for new agent instances:
export class ChatAgent extends Agent {
initialState = {
messages: [],
settings: { theme: "dark", notifications: true },
lastActive: null,
};
}
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,
};
}
Type safety
The second generic parameter to Agent defines your state type:
// State is fully typed
export class MyAgent extends Agent {
initialState = { count: 0 };
increment() {
// TypeScript knows this.state is MyState
this.setState({ count: this.state.count + 1 });
}
}
// 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 });
}
}
When initial state applies
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
class MyAgent extends Agent {
initialState = { count: 0 };
async onStart() {
// Safe to access - returns initialState if new, or persisted state
console.log("Current count:", this.state.count);
}
}
class MyAgent extends Agent<Env, { count: number }> {
initialState = { count: 0 };
async onStart() {
// Safe to access - returns initialState if new, or persisted state
console.log("Current count:", this.state.count);
}
}
Reading state
Access the current state via the this.state getter:
class MyAgent extends Agent {
async onRequest(request) {
// Read current state
const { players, status } = this.state;
if (status === "waiting" && players.length < 2) {
return new Response("Waiting for players...");
}
return Response.json(this.state);
}
}
class MyAgent extends Agent<
Env,
{ players: string[]; status: "waiting" | "playing" | "finished" }
> {
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 Response.json(this.state);
}
}
Undefined state
If you do not define initialState, this.state returns undefined:
export class MinimalAgent extends Agent {
// No initialState defined
async onConnect(connection) {
if (!this.state) {
// First time - initialize state
this.setState({ initialized: true });
}
}
}
export class MinimalAgent extends Agent {
// No initialState defined
async onConnect(connection: Connection) {
if (!this.state) {
// First time - initialize state
this.setState({ initialized: true });
}
}
}
Updating state
Use setState() to update state. This:
- Saves to SQLite (persistent)
- Broadcasts to all connected clients (excluding connections where
shouldSendProtocolMessagesreturnedfalse) - 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,
});
// 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 must be serializable
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
// Functions do not serialize
// Dates become strings, lose methods
// Circular references fail
// For dates, use ISO strings
this.setState({
createdAt: new Date().toISOString(),
});
// Good - plain objects, arrays, primitives
this.setState({
items: ["a", "b", "c"],
count: 42,
active: true,
metadata: { key: "value" },
});
// Bad - functions, classes, circular references
// Functions do not serialize
// Dates become strings, lose methods
// Circular references fail
// For dates, use ISO strings
this.setState({
createdAt: new Date().toISOString(),
});
Responding to state changes
Override onStateChanged() to react when state changes (notifications/side-effects):
class MyAgent extends Agent {
onStateChanged(state, source) {
console.log("State updated:", state);
console.log("Updated by:", source === "server" ? "server" : source.id);
}
}
class MyAgent extends Agent<Env, GameState> {
onStateChanged(state: GameState, source: Connection | "server") {
console.log("State updated:", state);
console.log("Updated by:", source === "server" ? "server" : source.id);
}
}
The source parameter
The source shows who triggered the update:
| Value | Meaning |
|---|---|
"server" |
Agent called setState() |
Connection |
A client pushed state via WebSocket |
This is useful for:
- Avoiding infinite loops (do not react to your own updates)
- Validating client input
- Triggering side effects only on client actions
class MyAgent extends Agent {
onStateChanged(state, source) {
// 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);
}
}
}
class MyAgent extends Agent<
Env,
{ status: "waiting" | "playing" | "finished" }
> {
onStateChanged(state: GameState, 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);
}
}
}
Common pattern: Client-driven actions
class MyAgent extends Agent {
onStateChanged(state, source) {
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,
),
});
}
}
}
class MyAgent extends Agent<Env, { messages: Message[] }> {
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,
),
});
}
}
}
Validating state updates
If you want to validate or reject state updates, override validateStateChange():
- Runs before persistence and broadcast
- Must be synchronous
- Throwing aborts the update
class MyAgent extends Agent {
validateStateChange(nextState, source) {
// Example: reject negative scores
if (nextState.score < 0) {
throw new Error("score cannot be negative");
}
// Example: only allow certain status transitions
if (this.state.status === "finished" && nextState.status !== "finished") {
throw new Error("Cannot restart a finished game");
}
}
}
class MyAgent extends Agent<Env, GameState> {
validateStateChange(nextState: GameState, source: Connection | "server") {
// Example: reject negative scores
if (nextState.score < 0) {
throw new Error("score cannot be negative");
}
// Example: only allow certain status transitions
if (this.state.status === "finished" && nextState.status !== "finished") {
throw new Error("Cannot restart a finished game");
}
}
}
Note
onStateChanged() is not intended for validation; it is a notification hook and should not block broadcasts. Use validateStateChange() for validation.
Client-side state sync
State synchronizes automatically with connected clients.
React (useAgent)
import { useAgent } from "agents/react";
function GameUI() {
const agent = useAgent({
agent: "game-agent",
name: "room-123",
onStateUpdate: (state, source) => {
console.log("State updated:", state);
},
});
// Push state to agent
const addPlayer = (name) => {
agent.setState({
...agent.state,
players: [...agent.state.players, name],
});
};
return <div>Players: {agent.state?.players.join(", ")}</div>;
}
import { useAgent } from "agents/react";
function GameUI() {
const agent = useAgent({
agent: "game-agent",
name: "room-123",
onStateUpdate: (state, source) => {
console.log("State updated:", state);
}
});
// Push state to agent
const addPlayer = (name: string) => {
agent.setState({
...agent.state,
players: [...agent.state.players, name]
});
};
return <div>Players: {agent.state?.players.join(", ")}</div>;
}
Vanilla JS (AgentClient)
import { AgentClient } from "agents/client";
const client = new AgentClient({
agent: "game-agent",
name: "room-123",
onStateUpdate: (state) => {
document.getElementById("score").textContent = state.score;
},
});
// Push state update
client.setState({ ...client.state, score: 100 });
import { AgentClient } from "agents/client";
const client = new AgentClient({
agent: "game-agent",
name: "room-123",
onStateUpdate: (state) => {
document.getElementById("score").textContent = state.score;
},
});
// Push state update
client.setState({ ...client.state, score: 100 });
State flow
flowchart TD
subgraph Agent
S["this.state<br/>(persisted in SQLite)"]
end
subgraph Clients
C1["Client 1"]
C2["Client 2"]
C3["Client 3"]
end
C1 & C2 & C3 -->|setState| S
S -->|broadcast via WebSocket| C1 & C2 & C3
State from Workflows
When using Workflows, you can update agent state from workflow steps:
// In your workflow
class MyWorkflow extends Workflow {
async run(event, step) {
// 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;
}
}
```ts // In your workflow class MyWorkflow extends Workflow<Env> { async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) { // Replace entire state await step.updateAgentState({ status: "processing", progress: 0 });
