State & Scheduling
Fetch https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/ and https://developers.cloudflare.com/agents/api-reference/schedule-tasks/ for complete documentation.
State Management
State persists to SQLite and broadcasts to connected clients automatically.
Define Typed State
type State = {
count: number;
items: string[];
};
export class MyAgent extends Agent<Env, State> {
initialState: State = { count: 0, items: [] };
}
Read and Update
// Read (lazy-loaded from SQLite)
const count = this.state.count;
// Write (sync, persists, broadcasts)
this.setState({ count: this.state.count + 1 });
Validation Hook
validateStateChange() runs synchronously before state persists. Throw to reject the update.
validateStateChange(nextState: State, source: Connection | "server") {
if (nextState.count < 0) {
throw new Error("Count cannot be negative");
}
}
Execution Order
validateStateChange(nextState, source)- sync, gating- State persisted to SQLite
- State broadcast to connected clients
onStateUpdate(nextState, source)- async viactx.waitUntil, non-gating
Client-Side Sync (React)
import { useAgent } from "agents/react";
function App() {
const [state, setLocalState] = useState<State>({ count: 0 });
const agent = useAgent<State>({
agent: "MyAgent",
name: "instance-1",
onStateUpdate: (newState) => setLocalState(newState)
});
return <button onClick={() => agent.setState({ count: state.count + 1 })}>
Count: {state.count}
</button>;
}
SQL API
Direct SQLite access for custom queries:
// Create table
this.sql`
CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
name TEXT,
created_at INTEGER DEFAULT (unixepoch())
)
`;
// Insert
this.sql`INSERT INTO items (id, name) VALUES (${id}, ${name})`;
// Query with types
const items = this.sql<{ id: string; name: string }>`
SELECT * FROM items WHERE name LIKE ${`%${search}%`}
`;
Scheduling
Schedule Types
| Mode | Syntax | Use Case |
|---|---|---|
| Delay | this.schedule(60, ...) |
Run in 60 seconds |
| Date | this.schedule(new Date(...), ...) |
Run at specific time |
| Cron | this.schedule("0 8 * * *", ...) |
Recurring schedule |
| Interval | this.scheduleEvery(30, ...) |
Fixed interval (every 30s) |
Examples
// Delay (seconds)
await this.schedule(60, "checkStatus", { id: "abc123" });
// Specific date
await this.schedule(new Date("2025-12-25T00:00:00Z"), "sendGreeting", { to: "user" });
// Cron (recurring)
await this.schedule("0 9 * * 1-5", "weekdayReport", {});
// Fixed interval (every 30 seconds, overlap prevention built-in)
await this.scheduleEvery(30, "pollUpdates");
await this.scheduleEvery(300, "syncData", { source: "api" });
Handler
async sendGreeting(payload: { to: string }, schedule: Schedule) {
console.log(`Sending greeting to ${payload.to}`);
// Cron schedules auto-reschedule; one-time schedules are deleted
}
Manage Schedules
const schedules = this.getSchedules();
const crons = this.getSchedules({ type: "cron" });
await this.cancelSchedule(schedule.id);
Retry on Schedules
await this.schedule(60, "task", payload, { retry: { maxAttempts: 3 } });
await this.scheduleEvery(30, "poll", undefined, { retry: { maxAttempts: 2 } });
Lifecycle Callbacks
export class MyAgent extends Agent<Env, State> {
async onStart() {
// Agent started or woke from hibernation
}
onConnect(conn: Connection, ctx: ConnectionContext) {
// WebSocket connected
}
onMessage(conn: Connection, message: WSMessage) {
// WebSocket message (non-RPC)
}
onStateUpdate(state: State, source: Connection | "server") {
// State changed (async, non-blocking)
}
onError(error: unknown) {
// Error handler
throw error; // Re-throw to propagate
}
}