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
.
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); }}
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: "server" | Connection) { 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 stateexport 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, }); }}
import { Agent } from "agents-sdk";
interface Env {}
// Define a type for your Agent's stateinterface FlightRecord { id: string; departureIata: string; arrival: Date;; arrivalIata: string; price: number;}
// Pass in the type of your Agent's stateexport class MyAgent extends Agent<Env, FlightRecord> { // This allows this.setState and the onStateUpdate method to // be typed: async onStateUpdate(state: FlightRecord) { console.log("state updated", state); }
async someOtherMethod() { this.setState({ ...this.state, price: this.state.price + 10, }); }}
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> );}
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.
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); }}
export class MyAgent extends Agent<Env> { async onRequest(request: 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.
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; } }}
export class ReasoningAgent extends Agent<Env> { async callReasoningModel(prompt: Prompt) { let result = this.sql<History>`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.
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Products
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark