Store and sync state·续

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

```js
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);
    }
}
export class MyAgent extends Agent {
    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] = 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:

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);
    }
}
type User = {
    id: string;
    name: string;
    email: string;
};

export class MyAgent extends Agent {
    async onRequest(request: 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<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.

Note

Providing a type parameter does not validate that the result matches your type definition. 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. 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:

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

// 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 {
    onStateChanged(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,
                ),
            });
        }
    }
}
// Client-side
function sendMessage(text: string) {
    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&lt;Env, { messages: Message[] }> {
    onStateChanged(state: GameState, 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,
                ),
            });
        }
    }
}

State vs SQL

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 {
    // 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,
        });
    }
}
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: 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:

// 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; // 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:

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 systemPrompt = prompt.system || "You are a helpful assistant.";
        const userPrompt = `${prompt.user}\n\nUser history:\n${context.join("\n")}`;

try {
            const response = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", {
                messages: [
                    { role: "system", content: systemPrompt },
                    { role: "user", content: userPrompt },
                ],
            });

// Store the response in history
            this
                .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${response.response})`;

return response.response;
        } catch (error) {
            console.error("Error calling reasoning model:", error);
            throw error;
        }
    }
}
interface Env {
    AI: Ai;
}

export class ReasoningAgent extends Agent&lt;Env> {
    async callReasoningModel(prompt: Prompt) {
        let result = this
            .sql&lt;History>`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 systemPrompt = prompt.system || "You are a helpful assistant.";
        const userPrompt = `${prompt.user}\n\nUser history:\n${context.join("\n")}`;

try {
            const response = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", {
                messages: [
                    { role: "system", content: systemPrompt },
                    { role: "user", content: userPrompt },
                ],
            });

// Store the response in history
            this
                .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${response.response})`;

return response.response;
        } 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

Property Type Description
state State Current state (getter)
initialState State Default state for new agents

Methods

Method Signature Description
setState (state: State) => void Update state, persist, and broadcast
onStateChanged (state: State, source: Connection \| "server") => void Called when state changes
validateStateChange (nextState: State, source: Connection \| "server") => void Validate before persistence (throw to reject)

Workflow step methods

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

Next steps

Agents API

Complete API reference for the Agents SDK.

Build a chat agent

Build and deploy an AI chat agent.

WebSockets

Build interactive agents with real-time data streaming.

Run Workflows

Orchestrate asynchronous workflows from your agent.

Was this helpful?

YesNo

On this page

Store and sync state·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/lifecycle/state/#page","headline":"Store and sync state","description":"Persist and sync Agent state across clients in real time using setState, SQL storage, and bidirectional updates.","url":"https://developers.cloudflare.com/agents/runtime/lifecycle/state/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/runtime/lifecycle/state/og.png?v=886569493f00f3d5","dateModified":"2026-06-03","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Configure Wrangler bindings, environment variables, and type generation for a project using the Agents SDK. title: Configuration image: https://developers.cloudflare.com/agents/runtime/operations/configuration/og.png?v=933c898d421435fa


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.