Skip to content
Cloudflare Docs

Store and sync state

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.

JavaScript
import { Agent } from "agents";
export class GameAgent extends Agent {
// Default state for new agents
initialState = {
players: [],
score: 0,
status: "waiting",
};
// React to state changes
onStateUpdate(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],
});
}
}

Defining initial state

Use the initialState property to define default values for new agent instances:

JavaScript
export class ChatAgent extends Agent {
initialState = {
messages: [],
settings: { theme: "dark", notifications: true },
lastActive: null,
};
}

Type safety

The second generic parameter to Agent defines your state type:

JavaScript
// 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 });
}
}

When initial state applies

Initial state is applied lazily on first access, not on every wake:

  1. New agent - initialState is used and persisted
  2. Existing agent - Persisted state is loaded from SQLite
  3. No initialState defined - this.state is undefined
JavaScript
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);
}
}

Reading state

Access the current state via the this.state getter:

JavaScript
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);
}
}

Undefined state

If you do not define initialState, this.state returns undefined:

JavaScript
export class MinimalAgent extends Agent {
// No initialState defined
async onConnect(connection) {
if (!this.state) {
// First time - initialize state
this.setState({ initialized: true });
}
}
}

Updating state

Use setState() to update state. This:

  1. Saves to SQLite (persistent)
  2. Broadcasts to all connected clients
  3. Triggers onStateUpdate() (after broadcast; best-effort)
JavaScript
// 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:

JavaScript
// 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 onStateUpdate() to react when state changes (notifications/side-effects):

JavaScript
class MyAgent extends Agent {
onStateUpdate(state, source) {
console.log("State updated:", state);
console.log("Updated by:", source === "server" ? "server" : source.id);
}
}

The source parameter

The source shows who triggered the update:

ValueMeaning
"server"Agent called setState()
ConnectionA 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
JavaScript
class MyAgent extends Agent {
onStateUpdate(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);
}
}
}

Common pattern: Client-driven actions

JavaScript
class MyAgent extends Agent {
onStateUpdate(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,
),
});
}
}
}

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
JavaScript
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");
}
}
}

Client-side state sync

State synchronizes automatically with connected clients.

React (useAgent)

JavaScript
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>;
}

Vanilla JS (AgentClient)

JavaScript
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:

JavaScript
// 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;
}
}

These are durable operations - they persist even if the workflow retries.

SQL API

Every individual Agent instance has its own SQL (SQLite) database that runs within the same context as the Agent itself. This means that inserting or querying data within your Agent is effectively zero-latency: the Agent does not have to round-trip across a continent or the world to access its own data.

You can access the SQL API within any method on an Agent via this.sql. The SQL API accepts template literals:

JavaScript
export class MyAgent extends Agent {
async onRequest(request) {
let userId = new URL(request.url).searchParams.get("userId");
// 'users' is just an example here: you can create arbitrary tables and define your own schemas
// within each Agent's database using SQL (SQLite syntax).
let [user] = this.sql`SELECT * FROM users WHERE id = ${userId}`;
return Response.json(user);
}
}

You can also supply a TypeScript type argument to the query, which will be used to infer the type of the result:

JavaScript
export class MyAgent extends Agent {
async onRequest(request) {
let userId = new URL(request.url).searchParams.get("userId");
// Supply the type parameter to the query when calling this.sql
// This assumes the results returns one or more User rows with "id", "name", and "email" columns
const [user] = this.sql`SELECT * FROM users WHERE id = ${userId}`;
return Response.json(user);
}
}

You do not need to specify an array type (User[] or Array<User>), as this.sql will always return an array of the specified type.

The SQL API exposed to an Agent is similar to the one within Durable Objects. You can use the same SQL queries with the Agent's database. Create tables and query data, just as you would with Durable Objects or D1.

Best practices

Keep state small

State is broadcast to all clients on every change. For large data:

TypeScript
// 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}`;
}

Optimistic updates

For responsive UIs, update client state immediately:

JavaScript
// Client-side
function sendMessage(text) {
const optimisticMessage = {
id: crypto.randomUUID(),
text,
pending: true,
};
// Update immediately
agent.setState({
...agent.state,
messages: [...agent.state.messages, optimisticMessage],
});
// Server will confirm/update
}
// Server-side
class MyAgent extends Agent {
onStateUpdate(state, source) {
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,
),
});
}
}
}

State vs SQL

Use State ForUse SQL For
UI state (loading, selected items)Historical data
Real-time countersLarge collections
Active session dataRelationships
ConfigurationQueryable data
JavaScript
export class ChatAgent extends Agent {
// 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) {
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,
});
}
}

Avoid infinite loops

Be careful not to trigger state updates in response to your own updates:

TypeScript
// Bad - infinite loop
onStateUpdate(state: State) {
this.setState({ ...state, lastUpdated: Date.now() });
}
// Good - check source
onStateUpdate(state: State, source: Connection | "server") {
if (source === "server") return; // Do not react to own updates
this.setState({ ...state, lastUpdated: Date.now() });
}

Use Agent state as model context

You can combine the state and SQL APIs in your Agent with its ability to call AI models to include historical context within your prompts to a model. Modern Large Language Models (LLMs) often have very large context windows (up to millions of tokens), which allows you to pull relevant context into your prompt directly.

For example, you can use an Agent's built-in SQL database to pull history, query a model with it, and append to that history ahead of the next call to the model:

JavaScript
export class ReasoningAgent extends Agent {
async callReasoningModel(prompt) {
let result = this
.sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`;
let context = [];
for (const row of result) {
context.push(row.entry);
}
const client = new OpenAI({
apiKey: this.env.OPENAI_API_KEY,
});
// Combine user history with the current prompt
const systemPrompt = prompt.system || "You are a helpful assistant.";
const userPrompt = `${prompt.user}\n\nUser history:\n${context.join("\n")}`;
try {
const completion = await client.chat.completions.create({
model: this.env.MODEL || "o3-mini",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
temperature: 0.7,
max_tokens: 1000,
});
// Store the response in history
this
.sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${completion.choices[0].message.content})`;
return completion.choices[0].message.content;
} catch (error) {
console.error("Error calling reasoning model:", error);
throw error;
}
}
}

This works because each instance of an Agent has its own database, and the state stored in that database is private to that Agent: whether it is acting on behalf of a single user, a room or channel, or a deep research tool. By default, you do not have to manage contention or reach out over the network to a centralized database to retrieve and store state.

API reference

Properties

PropertyTypeDescription
stateStateCurrent state (getter)
initialStateStateDefault state for new agents

Methods

MethodSignatureDescription
setState(state: State) => voidUpdate state, persist, and broadcast
onStateUpdate(state: State, source: Connection | "server") => voidCalled when state changes
validateStateChange(nextState: State, source: Connection | "server") => voidValidate before persistence (throw to reject)

Workflow step methods

MethodDescription
step.updateAgentState(state)Replace agent state from workflow
step.mergeAgentState(partial)Merge partial state from workflow
step.resetAgentState()Reset to initialState from workflow

Next steps