Skip to content
Cloudflare Docs

Manage and sync state

Every Agent has built-in state management capabilities, including built-in storage and synchronization between the Agent and frontend applications. State within an Agent is:

  • Persisted across Agent restarts: data is permanently persisted within the Agent.
  • Automatically serialized/deserialized: you can store any JSON-serializable data.
  • Immediately consistent within the Agent: read your own writes.
  • Thread-safe for concurrent updates

Agent state is stored in a SQL database that is embedded within each individual Agent instance: you can interact with it using the higher-level this.setState API (recommended) or by directly querying the database with this.sql.

State API

Every Agent has built-in state management capabilities. You can set and update the Agent's state directly using this.setState:

import { Agent } from "agents-sdk";
export class MyAgent extends Agent {
// Update state in response to events
async incrementCounter() {
this.setState({
...this.state,
counter: this.state.counter + 1,
});
}
// Handle incoming messages
async onMessage(message) {
if (message.type === "update") {
this.setState({
...this.state,
...message.data,
});
}
}
// Handle state updates
onStateUpdate(state, source) {
console.log("state updated", state);
}
}

If you're using TypeScript, you can also provide a type for your Agent's state by passing in a type as a type parameter as the second type parameter to the Agent class definition.

import { Agent } from "agents-sdk";
// Define a type for your Agent's state
// Pass in the type of your Agent's state
export class MyAgent extends Agent {
// This allows this.setState and the onStateUpdate method to
// be typed:
async onStateUpdate(state) {
console.log("state updated", state);
}
async someOtherMethod() {
this.setState({
...this.state,
price: this.state.price + 10,
});
}
}

Synchronizing state

Clients can connect to an Agent and stay synchronized with its state using the React hooks provided as part of agents-sdk/react.

A React application can call useAgent to connect to a named Agent over WebSockets at

import { useState } from "react";
import { useAgent } from "agents-sdk/react";
function StateInterface() {
const [state, setState] = useState({ counter: 0 });
const agent = useAgent({
agent: "thinking-agent",
name: "my-agent",
onStateUpdate: (newState) => setState(newState),
});
const increment = () => {
agent.setState({ counter: state.counter + 1 });
};
return (
<div>
<div>Count: {state.counter}</div>
<button onClick={increment}>Increment</button>
</div>
);
}

The state synchronization system:

  • Automatically syncs the Agent's state to all connected clients
  • Handles client disconnections and reconnections gracefully
  • Provides immediate local updates
  • Supports multiple simultaneous client connections

Common use cases:

  • Real-time collaborative features
  • Multi-window/tab synchronization
  • Live updates across multiple devices
  • Maintaining consistent UI state across clients
  • When new clients connect, they automatically receive the current state from the Agent, ensuring all clients start with the latest data.

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 doesn't 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, and

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 = await this.sql`SELECT * FROM users WHERE id = ${userId}`;
return Response.json(user);
}
}

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

type User = {
id: string;
name: string;
email: string;
};
export class MyAgent extends Agent<Env> {
async onRequest(request: Request) {
let userId = new URL(request.url).searchParams.get('userId');
// Supply the type paramter 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 = await this.sql<User>`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.

Providing a type parameter does not validate that the result matches your type definition. In TypeScript, properties (fields) that do not exist or conform to the type you provided will be dropped. If you need to validate incoming events, we recommend a library such as zod or your own validator logic.

The SQL API exposed to an Agent is similar to the one within Durable Objects: Durable Object SQL methods available on this.ctx.storage.sql. 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.

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:

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 await (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, the state stored in that database is private to that Agent: whether it's acting on behalf of a single user, a room or channel, or a deep research tool. By default, you don't have to manage contention or reach out over the network to a centralized database to retrieve and store state.