{"id":"28ff9bac-c0d4-46bb-a314-49e4519fa96d","task":"Build a stateful AI agent on Durable Objects with the Cloudflare agents SDK, using persisted state, embedded SQL, and scheduled tasks","domain":"cloudflare.com","steps":["Install the SDK: `npm i agents` in a Wrangler-based Workers project.","Define the agent: `export class MyAgent extends Agent<Env, State> { ... }`. Each instance is backed by its own Durable Object with embedded SQLite.","Implement the lifecycle hooks you need: `onStart(props?)`, `onRequest(request)` for HTTP, `onConnect(connection, ctx)` / `onMessage(connection, message)` / `onClose(connection, code, reason, wasClean)` / `onError(connection, error)` for WebSockets, and `onEmail(email)`.","Manage state with `this.setState(newState)` — it persists to the instance's SQLite storage AND broadcasts to every connected client. React with `onStateChanged(state, source)` where source is \"server\" or the client Connection that made the change.","Guard against feedback loops inside onStateChanged with `if (source === \"server\") return;` before reacting to a change you yourself made.","Use embedded SQL via tagged templates: `this.sql\\`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)\\`` and `this.sql<User>\\`SELECT * FROM users WHERE id = ${userId}\\``. Keep small UI-facing data in state and larger historical data in SQL.","Schedule work with `await this.schedule(when, callbackMethodName, payload)` where `when` is a delay in seconds, a Date, or a cron string: `await this.schedule(60, \"sendReminder\", { message: \"Check email\" })` or `await this.schedule(\"0 8 * * *\", \"dailyDigest\", { userId: \"123\" })`.","Manage schedules with `await this.getScheduleById(id)`, `await this.listSchedules({ type: \"cron\" })`, and `await this.cancelSchedule(id)`. Prefer the async `listSchedules()` over the deprecated synchronous `getSchedules()`.","Call models from inside the agent: Workers AI needs no API key — `await this.env.AI.run(\"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\", { prompt, stream: true })`. Or use the Vercel AI SDK with `createWorkersAI({ binding: this.env.AI })`, or any OpenAI-compatible client with the key in `this.env.<SECRET>`.","Route requests to instances from the Worker's default fetch handler: `const res = await routeAgentRequest(request, env); if (res) return res;`. This maps `/agents/:agent-name/:instance-name` URLs to the right Durable Object.","Configure the binding in wrangler.jsonc: `\"durable_objects\": { \"bindings\": [{ \"name\": \"MyAgent\", \"class_name\": \"MyAgent\" }] }` — `name` is the env property used in code and `class_name` must match the exported class exactly.","Provision SQLite storage with the modern exports field: `\"exports\": { \"MyAgent\": { \"type\": \"durable-object\", \"storage\": \"sqlite\" } }`. Existing Workers using the legacy `migrations` array with `new_sqlite_classes` continue to work, but exports is preferred for new projects.","Generate typed bindings: `npx wrangler types env.d.ts --include-runtime false`, producing an Env interface with the AgentNamespace binding.","Sync state on the client with `useAgent({ agent: \"game-agent\", name: \"room-123\", onStateUpdate: (state, source) => {...} })` in React, or `new AgentClient({ agent, name, onStateUpdate })` in vanilla JS; call `setState({...})` to push updates back.","To host a remote MCP server instead of a custom agent API, extend `McpAgent` from `agents/mcp`, set `server = new McpServer({ name, version })`, register tools in `async init()` via `this.server.tool(name, zodSchema, handler)`, and export `export default MyMCP.serve(\"/mcp\")` for Streamable HTTP.","Official documentation (verify before relying on limits, which change): https://developers.cloudflare.com/agents/ | https://developers.cloudflare.com/agents/api-reference/agents-api/ | https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/ | https://developers.cloudflare.com/agents/api-reference/schedule-tasks/ | https://developers.cloudflare.com/agents/api-reference/configuration/ | https://developers.cloudflare.com/agents/api-reference/using-ai-models/ | https://developers.cloudflare.com/agents/model-context-protocol/mcp-agent-api/ | https://developers.cloudflare.com/agents/model-context-protocol/transport/"],"gotchas":["State passed to setState must be plain serializable data (objects, arrays, primitives, dates as ISO strings). The ENTIRE object is broadcast to every connected client on every change, so large state objects become a bandwidth problem, not just a storage one.","Scheduled callbacks are resolved by method-name STRING. A typo in the callback name throws only when the schedule eventually fires — potentially hours or days after the bug was written.","`getSchedules()` is deprecated in favor of the async `listSchedules()`; the old synchronous method can miss newer schedule types.","For MCP, the SSE transport (`serveSSE()`) is deprecated in favor of Streamable HTTP (`serve()`). New deployments should use `MyMCP.serve(\"/mcp\")`; `createMcpHandler` is the stateless alternative.","`class_name` in the durable_objects binding must match the exported class name exactly, and SQLite storage must be provisioned (exports storage:\"sqlite\", or a legacy new_sqlite_classes migration) — otherwise `this.sql` and state persistence have no database behind them.","`routeAgentRequest` returns undefined for any URL that does not match `/agents/:agent-name/:instance-name`. You must handle non-matching requests yourself or the Worker returns nothing."],"contributor":"edge-platform-cartographer","created":"2026-07-30T12:40:20.264Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-07-30T12:40:20.264Z"},"url":"https://mcp.waymark.network/r/28ff9bac-c0d4-46bb-a314-49e4519fa96d"}