branch:
state.md
14945 bytesRaw
# State Management

Agents provide built-in state management with automatic persistence and real-time synchronization across all connected clients.

## Overview

Agent state is:

- **Persistent** - Automatically saved to SQLite, survives restarts and hibernation
- **Synchronized** - Changes broadcast to all connected WebSocket clients instantly
- **Bidirectional** - Both server and clients can update state
- **Type-safe** - Full TypeScript support with generics

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

```typescript
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<Env, State>` defines your state type:

```typescript
// 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`

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

```typescript
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 new Response(JSON.stringify(this.state));
}
```

### Undefined State

If you don't define `initialState`, `this.state` returns `undefined`:

```typescript
export class MinimalAgent extends Agent<Env> {
  // 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
3. Triggers `onStateChanged()` (after broadcast; best-effort)

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

```typescript
// Good - plain objects, arrays, primitives
this.setState({
  items: ["a", "b", "c"],
  count: 42,
  active: true,
  metadata: { key: "value" }
});

// Bad - functions, classes, circular references
this.setState({
  callback: () => {}, // Functions don't serialize
  date: new Date(), // Becomes string, loses methods
  self: this // Circular reference
});

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

```typescript
onStateChanged(state: GameState, source: Connection | "server") {
  console.log("State updated:", state);
  console.log("Updated by:", source === "server" ? "server" : source.id);
}
```

## 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**

```typescript
validateStateChange(nextState: GameState, source: Connection | "server") {
  // Example: reject negative scores
  if (nextState.score < 0) {
    throw new Error("score cannot be negative");
  }
}
```

> `onStateChanged()` is not intended for validation; it is a notification hook and should not block broadcasts.
>
> **Migration note:** `onStateChanged` replaces the deprecated `onStateUpdate` (server-side hook). If you're using `onStateUpdate` on your agent class, rename it to `onStateChanged` — the signature and behavior are identical. A console warning will fire once per class until you rename it.

### The `source` Parameter

The `source` tells you who triggered the update:

| Value        | Meaning                             |
| ------------ | ----------------------------------- |
| `"server"`   | Agent called `setState()`           |
| `Connection` | A client pushed state via WebSocket |

This is useful for:

- Avoiding infinite loops (don't react to your own updates)
- Validating client input
- Triggering side effects only on client actions

```typescript
onStateChanged(state: State, 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

```typescript
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
      )
    });
  }
}
```

## Client-Side State Sync

State synchronizes automatically with connected clients. See [Client SDK](./client-sdk.md) for full details.

### React (useAgent)

```typescript
import { useAgent } from "@cloudflare/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)

```typescript
import { AgentClient } from "@cloudflare/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

```
┌─────────────────────────────────────────────────────────────┐
│                         Agent                               │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                    this.state                       │    │
│  │              (persisted in SQLite)                  │    │
│  └─────────────────────────────────────────────────────┘    │
│           ▲                              │                  │
│           │ setState()                   │ broadcast        │
│           │                              ▼                  │
└───────────┼──────────────────────────────┼──────────────────┘
            │                              │
            │                              │ WebSocket
            │                              │
┌───────────┴──────────────────────────────┴───────────────────┐
│                        Clients                               │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐               │
│   │ Client 1 │    │ Client 2 │    │ Client 3 │               │
│   │  state   │    │  state   │    │  state   │               │
│   └──────────┘    └──────────┘    └──────────┘               │
│                                                              │
│   Any client can call setState() to push updates             │
└──────────────────────────────────────────────────────────────┘
```

## State from Workflows

When using [Workflows](./workflows.md), you can update agent state from workflow steps:

```typescript
// In your workflow
async run(event: AgentWorkflowEvent<Params>, step: AgentWorkflowStep) {
  // 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.

## Patterns & 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:

```typescript
// 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
onStateChanged(state: State, 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    |

```typescript
export class ChatAgent extends Agent<Env, State> {
  // 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:

```typescript
// 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;  // Don't react to own updates
  this.setState({ ...state, lastUpdated: Date.now() });
}
```

## 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 after state is persisted and broadcast |

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

- [Readonly Connections](./readonly-connections.md) - Restrict which connections can update state
- [Client SDK](./client-sdk.md) - Full client-side state sync documentation
- [Workflows](./workflows.md) - Durable state updates from workflows
- [SQL API](./sql.md) - When to use SQL instead of state