Claude Code Cloudflare Agents SDK CLAUDE.md Durable Objects AI Coding 2026

Claude Code for Cloudflare Agents SDK: CLAUDE.md Rules for State, Scheduling, and the Hibernation Trap (2026)

The Prompt Shelf ·

Cloudflare’s Agents SDK — the agents npm package for building stateful AI agents on top of Durable Objects — has no dedicated CLAUDE.md guide anywhere, including on this site. Our own Cloudflare Workers guide covers Wrangler bindings, D1, KV, and Durable Objects in general, but it stops at the platform layer — it doesn’t touch the Agent base class, setState, this.schedule, or the two different naming rules that govern how an Agent gets wired up and addressed. That gap matters more for this SDK than most, because it’s the one Cloudflare product most directly aimed at the exact thing this gallery is about: building AI agents, not just deploying serverless functions.

The SDK is also a moving target in 2026 — long-running sessions shipped natively in April, keepAlive() landed in March as an experimental heartbeat, and the preview “Project Think” work adds durable-execution primitives (fibers) for multi-step workflows. None of that is reflected in a model’s training data with any consistency, which is exactly the situation CLAUDE.md exists to correct.

Why the Cloudflare Workers Guide Doesn’t Cover This

A CLAUDE.md written from our Workers guide gets the runtime facts right — V8 isolates, no fs, bindings configured in wrangler.jsonc rather than imported as SDK clients. None of that is wrong for an Agents SDK project; it’s just incomplete. The Agents SDK adds a specific object model on top: a base Agent class with SDK-managed state that syncs to clients automatically, a scheduling API with its own callback-lookup mechanism, and a routing layer with a naming convention that doesn’t match the one in wrangler.jsonc. An agent that only knows “Durable Objects hold state” will still reach for this.someField = x instead of this.setState({...}), and won’t know why the value it just set didn’t show up on the connected frontend.

Three things make the Agents SDK worth its own section:

State only syncs and survives hibernation if it goes through setState. A Durable Object hibernates after a few idle seconds; an Agent is a Durable Object, so it hibernates the same way. SDK-managed state (initialState, setState()) and anything written via this.sql persist across that cycle. A plain instance field does not — it’s reinitialized from the class definition every time the Agent wakes up, which reads as “my agent randomly forgets things” if nobody told the agent building it which storage mechanism actually persists.

this.schedule() takes a callback as a string, not a function. this.schedule(60, this.remind) type-checks, looks correct, and does not call remind — the SDK resolves the callback by method name (keyof this) at the moment the task fires, not by reference at the moment it’s scheduled. Get the name wrong (or refactor the method and forget the string) and the failure surfaces whenever the task was due to run, not when the bad code was written.

Two unrelated naming rules govern how an Agent gets wired up. The durable_objects binding’s class_name in wrangler.jsonc must exactly match the exported class name — no conversion, case-sensitive, or the deploy fails outright. Separately, routeAgentRequest() converts that same class name to kebab-case to build the URL path an Agent is actually reachable at. These are two different transformations applied to the same string in two different places, and nothing in the error output when one of them is wrong points back to the other.

Agent vs Plain Durable Object vs Worker

## Architecture

This project uses Cloudflare's Agents SDK (`agents` npm package), not a
plain Durable Object. An Agent IS a Durable Object under the hood — the
SDK adds a base class with managed state sync, scheduling, callable RPC,
and WebSocket lifecycle hooks on top of it.

Use the Agent class for anything that: holds state across many turns of
interaction, exposes methods a frontend calls directly, or runs
scheduled/recurring work tied to that state (chat agents, task runners,
per-user assistants).

Do NOT reach for a new Agent class for stateless request handling, a
rate limiter, or a simple KV-backed cache — those stay plain Workers or
a hand-rolled Durable Object. Extending Agent for something that never
uses setState, schedule, or @callable() is unnecessary indirection.

The Agents SDK doesn’t replace the rest of the Workers platform — a project typically has both plain fetch handlers for stateless routes and one or more Agent classes for the stateful, agentic parts. CLAUDE.md should say which is which up front, because “just make it an Agent” is an easy default for a model to reach for once the class exists in the codebase at all.

wrangler.jsonc: One Exact Match, One Auto-Converted Path

{
  "name": "my-agent-worker",
  "main": "src/server.ts",
  "compatibility_date": "2026-08-24",
  "compatibility_flags": ["nodejs_compat"],
  "durable_objects": {
    "bindings": [
      { "name": "CounterAgent", "class_name": "CounterAgent" }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["CounterAgent"] }
  ]
}
## wrangler.jsonc Rules

- `durable_objects.bindings[].class_name` MUST exactly match the exported
  Agent class name (case-sensitive, no conversion). A mismatch fails at
  deploy time, not silently.
- A new Agent class needs a new entry in `migrations[].new_sqlite_classes`
  the first time it's deployed — forgetting this is the most common
  "why won't this Agent deploy" mistake.
- `compatibility_flags: ["nodejs_compat"]` is required for the Agents SDK.
- The URL path to reach an Agent is NOT the binding name as written —
  `routeAgentRequest()` converts the class name to kebab-case:
  class `CounterAgent` → path `/agents/counter-agent/:instance-name`.
  Don't hardcode the PascalCase class name into a client fetch URL or
  a `useAgent({ agent: "..." })` call.
import { Agent, routeAgentRequest, callable } from "agents";

export class CounterAgent extends Agent<Env, { count: number }> {
  // ...
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    return (
      (await routeAgentRequest(request, env)) ??
      new Response("Not found", { status: 404 })
    );
  },
} satisfies ExportedHandler<Env>;

routeAgentRequest reads the URL, extracts the kebab-cased agent name and instance name, looks up the matching Durable Object binding, and forwards the request — none of that routing logic needs to be written by hand, but it does mean the binding name in config and the URL segment a client actually calls are never the same string, and treating them as interchangeable is where the naming trap comes from.

State: setState Syncs and Persists, a Plain Field Doesn’t

## State Management

Agent state goes through `initialState` + `this.setState()`, never a
plain `this.someField = x` for anything that needs to survive hibernation
or sync to a connected client.

- `initialState` defines the shape and starting value.
- `this.setState({ ...patch })` updates it, persists it to the Agent's
  SQLite storage, and pushes the update to every connected WebSocket
  client automatically — no manual broadcast needed.
- `onStateUpdate(state, source)` runs when state changes, including on
  the client side via `useAgent`'s `onStateUpdate` callback.
- Data that doesn't need real-time client sync but still needs to
  survive hibernation (large logs, structured records) goes through
  `this.sql`, not a plain field either.
import { Agent } from "agents";

type CounterState = { count: number };

export class CounterAgent extends Agent<Env, CounterState> {
  initialState: CounterState = { count: 0 };

  // Wrong — resets to the class-defined initial value every time
  // this Agent hibernates and wakes back up, and never reaches
  // a connected client.
  private lastAccessedAt = Date.now();

  @callable()
  increment() {
    // Correct — persists across hibernation, syncs to clients
    this.setState({ count: this.state.count + 1 });
    return this.state.count;
  }
}
// Client — React
import { useAgent } from "agents/react";

function Counter() {
  const agent = useAgent<CounterAgent, CounterState>({
    agent: "counter-agent", // kebab-case, not "CounterAgent"
    onStateUpdate: (state) => console.log("count is now", state.count),
  });

  return <button onClick={() => agent.stub.increment()}>+1</button>;
}

The lastAccessedAt field above isn’t a contrived example — it’s the exact shape of the mistake an agent makes when it’s told “add a field to track X” without being told which storage mechanism the project actually uses. It compiles, it works during a live session, and it silently resets on the next cold wake, which in a low-traffic Agent can be minutes after it was set.

Scheduling: the Callback Is a String, Not a Reference

## Scheduled Tasks

`this.schedule(when, callback, payload?)``callback` MUST be a string
naming a method on this Agent class (`keyof this`), never a function
reference (`this.methodName`) or an arrow function.

- One-time, relative: `this.schedule(60, "sendReminder", payload)`
- One-time, absolute: `this.schedule(someDate, "sendReminder", payload)`
- Cron: `this.schedule("0 8 * * *", "dailyDigest")`
- Recurring interval: `this.scheduleEvery(300, "pollStatus")`

If the named method doesn't exist on the class, the task throws when it
FIRES, not when it's scheduled — check the method name is a real,
current method before trusting a schedule() call added by an agent.

Cron schedules default to `idempotent: true` (dedupes on identical
callback/expression/payload); one-time schedules default to `false`.
Cancel with `this.cancelSchedule(id)` before a task you no longer want
actually runs.
export class ReminderAgent extends Agent<Env, State> {
  @callable()
  async setReminder(minutesFromNow: number, note: string) {
    // Wrong — passes a function reference; does not call sendReminder
    // await this.schedule(minutesFromNow * 60, this.sendReminder, { note });

    // Correct — callback is the method name as a string
    const task = await this.schedule(minutesFromNow * 60, "sendReminder", { note });
    return task.id;
  }

  async sendReminder(payload: { note: string }) {
    // fires later, looked up by the string "sendReminder"
  }
}

Payloads have to be JSON-serializable and are capped at 2MB. A scheduled task survives the same hibernation cycle as the Agent’s state, because it’s stored in the same per-instance SQLite storage rather than an in-memory timer — that’s also why setTimeout/setInterval are the wrong tool inside an Agent method: they don’t survive the Durable Object being evicted from memory, which this.schedule is specifically built to do.

Callable Methods: Type-Safe RPC, Not a Fetch Route

## Callable Methods

Frontend-invokable Agent methods use the `@callable()` decorator. The
client calls them as `agent.stub.methodName(...)` via `useAgent`
don't build a separate `onRequest` HTTP route for something a connected
client can call directly through the callable interface.

Reserve `onRequest` for requests that aren't from an already-connected
Agent client (webhooks, external HTTP callers, health checks).
export class CounterAgent extends Agent<Env, CounterState> {
  @callable()
  increment() {
    this.setState({ count: this.state.count + 1 });
    return this.state.count;
  }

  // Not callable from the client — internal helper only
  private logAccess() {
    // ...
  }
}

WebSocket Lifecycle and What Hibernation Resets

## WebSocket Lifecycle

- `onConnect(connection, ctx)` — new WebSocket connection established.
- `onMessage(connection, message)` — message received on an open
  connection.

Hibernation is automatic and enabled by default: the Agent's Durable
Object is evicted from memory after a few idle seconds, but an open
WebSocket connection stays alive and the Agent re-instantiates on the
next message or scheduled event. State set via `setState`/`this.sql`
survives that cycle intact. A plain class field, a `Map`/`Set` built in
a constructor, or an in-memory cache does NOT — it's recreated from the
class definition on every wake, which looks identical to "this Agent
lost its data" from the outside.

Nothing about this is a bug — it’s the same Durable Objects Hibernation API that makes the platform cheap to run millions of low-traffic Agent instances on, since a hibernating Agent costs nothing while idle. The rule worth stating plainly in CLAUDE.md is narrower than “understand hibernation”: it’s “if a value needs to survive being idle, it goes through setState or this.sql, full stop,” because that’s the one sentence that actually changes what code gets generated.

Complete CLAUDE.md Template

# CLAUDE.md

## Project Overview
Cloudflare Workers project using the Agents SDK (`agents` npm package).
Agent classes extend `Agent<Env, State>` and run as Durable Objects.

## Commands
- Local dev: `wrangler dev` (Miniflare emulates Durable Objects/hibernation)
- Type check: `tsc --noEmit`
- Deploy: `wrangler deploy`

## Architecture
Agent classes hold stateful, agentic logic (state across turns, callable
methods, scheduled work). Plain fetch handlers/Workers stay stateless.
Don't create a new Agent class for stateless request handling.

## wrangler.jsonc
`durable_objects.bindings[].class_name` must exactly match the exported
class name (case-sensitive). New Agent classes need an entry in
`migrations[].new_sqlite_classes` on first deploy. `nodejs_compat` flag
required.

## Routing
The URL path to an Agent is the class name converted to kebab-case by
`routeAgentRequest` (`CounterAgent` -> `/agents/counter-agent/:name`),
NOT the binding name as written in wrangler.jsonc. Client `useAgent({
agent: "..." })` calls use the kebab-case form.

## State
`initialState` + `this.setState()` only, for anything that must survive
hibernation or sync to connected clients. No plain instance fields for
persistent/synced data — they reset on every hibernation wake.

## Scheduling
`this.schedule(when, callback, payload?)` — callback is a STRING method
name (`keyof this`), never a function reference. Verify the named method
still exists before trusting a schedule() call. Cron defaults to
idempotent; one-time schedules default to non-idempotent.

## Callable Methods
Client-invokable methods use `@callable()`, called via
`agent.stub.methodName()` on the frontend. `onRequest` is for non-client
HTTP callers only (webhooks, health checks).

## WebSockets
`onConnect`/`onMessage` for lifecycle. Hibernation is automatic — only
`setState`/`this.sql` data survives an idle-then-wake cycle.

## What to Avoid
- A plain `this.field = x` for data that needs to persist or sync — use
  `setState`.
- `this.schedule(60, this.methodName, ...)` — pass `"methodName"` as a
  string, not a reference.
- Hardcoding a PascalCase class name into a client URL/`useAgent` call —
  use the kebab-case route.
- `setTimeout`/`setInterval` inside an Agent for anything that must
  survive hibernation — use `this.schedule`/`this.scheduleEvery`.
- Extending `Agent` for logic that never uses state, scheduling, or
  callable methods — use a plain Worker or Durable Object instead.

CLAUDE.md vs AGENTS.md for an Agents SDK Project

AGENTS.md holds the tool-agnostic contract: the setState-vs-plain-field rule, the schedule callback-as-string requirement, the binding-name-vs-URL-path distinction, when to reach for an Agent class at all. A Cursor or Codex agent editing the same codebase needs the identical constraints — none of it is Claude-specific. CLAUDE.md adds how Claude Code specifically should operate on top: whether to run tsc --noEmit after editing an Agent class, how verbose to be when explaining a hibernation-related bug.

AGENTS.md   → state/scheduling/routing rules, when to use Agent vs Worker
CLAUDE.md   → "run tsc --noEmit after editing src/*.ts", verbosity preferences

A PostToolUse hook running tsc --noEmit after every Agent-class edit won’t catch the this.schedule(60, this.method, ...) mistake — it type-checks fine, since keyof this isn’t distinguished from a bound method reference by the compiler in every configuration. That one is a code-review-time rule, which is exactly why it belongs in CLAUDE.md/AGENTS.md as an explicit sentence rather than left for the type system to catch.

Agents SDK vs a Plain Durable Object in 2026

Not every stateful Cloudflare object should be an Agent. The SDK is purpose-built for agentic workloads — state that syncs to a UI in real time, scheduled work tied to that state, callable methods a frontend invokes directly — and its 2026 additions make that focus more pronounced: native long-running sessions (shipped April), an experimental keepAlive() heartbeat (March), and the preview “Project Think” durable-execution primitives (fibers) for multi-step workflows that can span days. None of that is aimed at a rate limiter or a WebSocket chat room with no scheduling or RPC needs — a plain Durable Object, without the Agent base class, is less abstraction for those. The practical rule for CLAUDE.md: reach for Agent when the object needs at least one of state-sync, scheduling, or callable RPC; otherwise a hand-rolled Durable Object (as covered in our Cloudflare Workers guide) is the simpler default.

Putting It Together

The Agents SDK’s traps share a shape with the rest of the Cloudflare platform’s Durable Objects model — a hibernation cycle that resets anything not explicitly persisted — but adds two SDK-specific ones on top: a scheduling API where the callback has to be spelled as a string, and a routing layer that silently kebab-cases the same class name that wrangler.jsonc requires to match exactly. None of these show up as an obviously broken diff. A plain field compiles and works right up until the Agent goes idle. A function-reference callback compiles and does nothing, with the failure surfacing only when the scheduled time arrives.

Start from the template above, confirm which parts of a given codebase are genuinely agentic (Agent class) versus plain stateless Workers routes, and layer in the project’s own model-calling conventions (Workers AI bindings, an external LLM API, MCP tool definitions via McpAgent) as a project-specific section underneath.

Real-world Cloudflare examples — including a hierarchical AGENTS.md for the workers-sdk monorepo — are browsable in our gallery.

FAQ

Why does my Cloudflare Agent lose data after being idle for a while?

Hibernation evicts the Durable Object from memory after a few idle seconds; setState/this.sql data persists through that, but a plain class field resets to its class-defined initial value on the next wake.

Why doesn’t my Cloudflare Agent’s scheduled task run?

this.schedule()’s callback argument is a string method name, not a function reference — passing this.methodName instead of "methodName" silently fails to schedule what it looks like it schedules.

What’s the difference between Cloudflare’s Agent class and a plain Durable Object?

An Agent is a Durable Object with a base class layered on top — managed state sync, this.schedule, @callable() RPC, and WebSocket lifecycle hooks — all still backed by the same per-instance SQLite storage and hibernation model.

Why do the Durable Objects binding name and the Agent’s URL path look different for the same class?

class_name in wrangler.jsonc must match the exported class name exactly with no conversion, while routeAgentRequest() separately kebab-cases that same class name to build the URL path — two unrelated transformations of the same string.

Should I use the Agents SDK or a plain Durable Object for a new Cloudflare project in 2026?

Use the Agents SDK when the object needs state-sync, scheduling, or callable RPC tied to agentic behavior; a plain Durable Object is less abstraction for anything that doesn’t.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules