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:

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

  1. Saves to SQLite (persistent)
  2. Broadcasts to all connected clients (excluding connections where shouldSendProtocolMessages returned false)
  3. 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 });