Cloudflare Agents SDK: Building Stateful AI Agents on the Edge

Anyone can make simple LLM API calls and receive a response. The real challenge comes next. Building a “stateful production agent”—one that remembers past conversations, calls external tools, pauses for human approvals before resuming, and triggers autonomous tasks on a cron schedule—requires stitching together state management, persistent storage, real-time communication, and long-running workflows. Operating separate instances of Redis, PostgreSQL, a WebSocket server, message queues, and cron jobs makes your infrastructure far more complex than the agent itself.
Announced during Cloudflare’s Agents Week in 2026, the Agents SDK solves this complexity on a single Durable Object. Each agent instance is mapped 1:1 to a Durable Object, persisting state in built-in SQLite, communicating with frontends in real time via WebSockets, and running autonomously using alarms and crons. With zero extra infrastructure, a single wrangler deploy deploys your agent across more than 300 global PoPs.
This guide covers everything you need to ship a production-ready agent: Agents SDK architecture, Agent class implementation, type-safe RPC using the @callable() decorator, React integration with useAgent/useAgentChat hooks, Human-in-the-Loop approval patterns, and key limits and costs for production deployment.
Key Takeaways
- Agents SDK agents run on Durable Objects, offering up to 1GB of built-in SQLite per instance, 30 seconds of CPU time (reset per request), and unlimited wall-clock waiting.
- By extending the
Agentclass and exposing methods with@callable(), frontends can instantly make type-safe WebSocket RPC calls using theuseAgenthook.- Human-in-the-Loop supports gating tool calls with the
needsApprovalflag or delaying execution for days or months using Cloudflare Workflows’waitForApproval().- Agents can trigger tasks autonomously via cron schedules and alarms, escaping the standard request-response paradigm.
- Billing follows standard Durable Objects pricing on Workers Paid plans, supporting tens of millions of concurrent agent instances per account.
Agents SDK Architecture: Why Durable Objects?
Traditional serverless AI agent implementations mostly rely on a “stateless function + external storage” pattern. You invoke LLMs in AWS Lambda or Cloud Functions, store conversation history in Redis or DynamoDB, track long-running state in Step Functions, and delegate real-time communication to a separate WebSocket server. Orchestrating 4–5 different services for a single agent results in glue code for synchronization that easily outgrows the actual agent logic.
The Agents SDK collapses this stack into a single Durable Object. According to the official Cloudflare Architecture documentation, an agent instance is structured as follows:
| Role | Provided by Durable Objects | Traditional Equivalent |
|---|---|---|
| Compute | Single-threaded isolate, 30s CPU time per request | Lambda / Cloud Run |
| Persistent State | Built-in SQLite (Up to 1GB per instance) | DynamoDB / Redis |
| Real-time Communication | Native WebSockets (with Hibernation support) | Pusher / Socket.IO Server |
| Scheduling | Alarm API + Cron expressions | CloudWatch Events / Cron Server |
| Global Routing | Route to the exact same instance globally via unique ID | Route 53 + Load Balancer |
| Fault Tolerance | Automatic migration and restart on another PoP during failures | Custom implementation |
The key insight is the 1:1 mapping of “Agent instance = Durable Object instance”. Calling this.setState() immediately persists changes to built-in SQLite and automatically broadcasts the update to all connected WebSocket clients. While waiting for an LLM response, the Durable Object can wait indefinitely in wall-clock time (without consuming CPU time), allowing you to process long-running workflows inside the agent without external queues or Step Functions.
The practical advantage of this architecture is the elimination of deployment complexity. Running wrangler deploy once deploys your agent code, state storage, WebSocket endpoints, and schedulers to Cloudflare’s global network all at once. You don’t need to provision databases, scale WebSocket servers, or configure external cron jobs.
Agent Class Implementation: State Management and @callable RPC
Creating an agent starts by extending the Agent class. Let’s create a project using the agents-starter template on GitHub and examine its core structure.
# Create project (latest template as of 2026-08)
npm create cloudflare@latest -- --template cloudflare/agents-starter my-agent
cd my-agent
Below is a server-side implementation example for a customer support ticket agent. This pattern handles state persistence, type-safe RPC, and tool calling within a single class:
// src/server.ts
import { Agent, callable } from "agents";
import { AIChatAgent } from "@cloudflare/ai-chat";
// Define agent state interface
export interface TicketState {
ticketId: string | null;
status: "idle" | "investigating" | "waiting-approval" | "resolved";
history: Array<{ role: string; content: string; timestamp: number }>;
metadata: Record<string, string>;
}
export class SupportAgent extends AIChatAgent<Env, TicketState> {
// Initial state — automatically persisted to SQLite upon Durable Object creation
initialState: TicketState = {
ticketId: null,
status: "idle",
history: [],
metadata: {},
};
// Methods exposed with @callable() can be called from frontend via agent.stub.assignTicket()
@callable()
async assignTicket(ticketId: string, priority: string): Promise<string> {
// setState() writes immediately to SQLite + broadcasts to all connected clients
this.setState({
...this.state,
ticketId,
status: "investigating",
metadata: { ...this.state.metadata, priority },
});
// Analyze ticket using Workers AI (inference on the edge)
const analysis = await this.env.AI.run("@cf/meta/llama-3.3-70b-instruct-fp8-fast", {
messages: [
{ role: "system", content: "Analyze customer support ticket and suggest solutions." },
{ role: "user", content: `Ticket ${ticketId}, priority: ${priority}` },
],
});
return analysis.response ?? "Analysis complete";
}
@callable()
async getStatus(): Promise<TicketState> {
return this.state;
}
// Cron schedule: check unresolved tickets every hour
async onCronTrigger(cron: string): Promise<void> {
if (this.state.status === "investigating") {
const elapsed = Date.now() - (this.state.history.at(-1)?.timestamp ?? 0);
if (elapsed > 3600_000) {
// Unresolved for > 1 hour -> escalation notification
await this.escalate();
}
}
}
private async escalate(): Promise<void> {
this.setState({ ...this.state, metadata: { ...this.state.metadata, escalated: "true" } });
// Notification dispatch logic (Slack webhooks, email, etc.)
}
}
Note: The @callable() decorator is exclusively for WebSocket-based RPC. When calling agent methods within the same Worker or from another Durable Object, use standard Durable Object RPC (this.env.AGENT.get(id)). Using @callable() for internal calls adds unnecessary WebSocket overhead.
Don’t forget to bind your agent as a Durable Object in wrangler.jsonc:
// wrangler.jsonc
{
"name": "support-agent",
"main": "src/index.ts",
"compatibility_date": "2026-07-28",
"durable_objects": {
"bindings": [
{
"name": "SUPPORT_AGENT",
"class_name": "SupportAgent"
}
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["SupportAgent"] }
],
"ai": { "binding": "AI" }
}
React Frontend Integration: useAgent and useAgentChat Hooks
The Agents SDK provides dedicated React hooks to manage frontend-agent connections declaratively. The official React Integration Guide details three hooks with distinct responsibilities:
| Hook | Package | Purpose |
|---|---|---|
useAgent |
agents/react |
General-purpose agent connection. State sync + RPC calls |
useAgentChat |
@cloudflare/ai-chat/react |
Chat-optimized. Message persistence, streaming, auto-executes tools |
useVoiceAgent |
@cloudflare/voice |
Voice STT/TTS. Real-time voice over the same WebSocket |
Below is an example React component connecting to SupportAgent using useAgent. This pattern handles WebSocket connection management, state auto-synchronization, and type-safe RPC calls declaratively:
// src/client.tsx
import { useAgent } from "agents/react";
import { useState, useCallback } from "react";
import type { SupportAgent, TicketState } from "./server";
function SupportDashboard() {
const [ticket, setTicket] = useState<TicketState | null>(null);
const [ticketId, setTicketId] = useState("");
// useAgent automatically manages WebSocket connection,
// and onStateUpdate is triggered whenever the agent calls setState()
const agent = useAgent<SupportAgent, TicketState>({
agent: "SupportAgent",
onStateUpdate: (state) => setTicket(state),
onError: (error) => console.error("Agent connection error:", error),
});
const handleAssign = useCallback(async () => {
if (!agent.stub || !ticketId) return;
// Type-safe RPC call — only @callable() methods are exposed on stub
const result = await agent.stub.assignTicket(ticketId, "high");
console.log("Analysis result:", result);
}, [agent.stub, ticketId]);
return (
<div>
<input
value={ticketId}
onChange={(e) => setTicketId(e.target.value)}
placeholder="Enter ticket ID"
/>
<button onClick={handleAssign} disabled={!agent.connected}>
Assign Ticket
</button>
{ticket && (
<div>
<p>Status: {ticket.status}</p>
<p>Priority: {ticket.metadata.priority ?? "Unassigned"}</p>
</div>
)}
</div>
);
}
useAgentChat is a specialized hook that handles SQLite message persistence, LLM response streaming, and automatic tool call inlining out of the box. Using useAgentChat instead of useAgent for chat interfaces reduces your code by more than half.
Human-in-the-Loop: Tool Approval Gating and Long-Waiting Patterns
One of the trickiest parts of building production AI agents is requiring human approval before executing sensitive operations. The Agents SDK supports this across three distinct patterns:
Pattern 1: Tool Gating with needsApproval. By setting needsApproval: true in your tool definition, the agent pauses execution and transitions to an approval-requested state whenever it attempts to call that tool. Once the frontend submits an approval or rejection, the agent either resumes execution or aborts.
// Example tool definition — applying approval gate to payment refund tool
const tools = [
{
name: "process_refund",
description: "Process a refund for a customer",
parameters: {
type: "object",
properties: {
amount: { type: "number", description: "Refund amount (USD)" },
reason: { type: "string", description: "Refund reason" },
},
required: ["amount", "reason"],
},
// Key flag — pauses execution when agent attempts to call this tool
needsApproval: true,
},
];
Note: needsApproval is best suited for in-session real-time approvals. Because closing the browser terminates the WebSocket connection, it isn’t ideal for scenarios where approvals might take hours or days.
Pattern 2: Cloudflare Workflows’ waitForApproval(). For long-running waiting scenarios (e.g., waiting for manager approval overnight), use the waitForApproval() method in Cloudflare Workflows. This method establishes a persistent gate on top of the Durable Object. Even if the agent hibernates and unloads from memory, receiving the approval event automatically wakes up the agent to resume the workflow. This allows for wait periods spanning days or even months.
Pattern 3: MCP Elicitation. When your agent interacts with Model Context Protocol (MCP) servers, the MCP server can prompt users for additional information via configureElicitationHandlers(). This is particularly useful when your agent acts as an MCP client orchestrating multiple external tools.
Scheduling and Autonomous Execution: Crons, Alarms, and Webhooks
Traditional AI chatbots only respond when a user sends a message. Production agents must be able to initiate tasks autonomously. Agents SDK agents inherit the Durable Objects Alarm API to support several autonomous execution patterns:
| Trigger | Configuration | Use Case |
|---|---|---|
| Cron Schedule | triggers.crons in wrangler.jsonc |
Periodic hourly/daily jobs (reports, monitoring) |
| Alarms | this.ctx.storage.setAlarm(timestamp) |
One-time execution at a specific timestamp (scheduled dispatches, reminders) |
| Webhook Handling | Route to agent from Worker fetch handler |
Reacting to external events (Stripe payments, GitHub PR merges) |
| Email Handling | email() handler |
Automated email classification & response |
The key advantage of this architecture is zero cost while idle. Thanks to Durable Objects’ Hibernation API, an agent can unload from memory while keeping WebSocket connections open. When an alarm or cron triggers, it automatically wakes up, executes the task, and goes back to sleep. Because you don’t need always-on VM instances, deploying thousands of concurrent agents incurs charges only when agents are actively processing.
// Cron trigger configuration (wrangler.jsonc)
{
"triggers": {
"crons": ["0 */6 * * *"] // Run every 6 hours
}
}
Production Deployment: Limits, Costs, and Operations Checklist
Here are the essential limits and pricing structures you must evaluate before deploying Agents SDK to production. Figures are based on the Cloudflare Workers Pricing Page and Durable Objects Pricing Page as of August 2026.
| Item | Limit / Value | Notes |
|---|---|---|
| Concurrent agent instances per account | Tens of millions+ | Hard limit can be increased upon request |
| Max state size per instance | 1GB (SQLite) | Key-Value storage can also be used separately |
| CPU time per request | 30 seconds | Resets per HTTP request or WebSocket message |
| Wall-clock wait time | Unlimited | Waiting for LLM responses does not consume CPU time |
| Max agent definitions | ~250,000 | Account-wide |
| Durable Objects Pricing (Paid) | $0.15 per 1M requests + $12.50/GB-month | Wall-clock time during Hibernation is free |
| Workers AI Inference | Model-dependent | Llama 3.3 70B: $0.27 per 1M input tokens |
Three operational caveats to keep in mind:
-
Prune state to stay below the 1GB SQLite limit. Unbounded conversation history will eventually hit the limit. Implement a strategy to archive old logs to Cloudflare R2 while retaining only the latest N messages in SQLite.
-
Keep the 30-second CPU limit in mind. While LLM API calls are asynchronous
awaitwaits that consume zero CPU time, complex post-processing or parsing could exceed the 30-second CPU limit. Offload heavy computation to separate Workers or process asynchronously with Cloudflare Queues. -
Leverage Hibernation aggressively. Using
hibernateWebSocket()allows the agent to unload from memory between messages even while maintaining active WebSocket connections. This ensures GB-second duration charges apply only during active processing, dramatically reducing costs. We detail the cost-saving mechanics of this pattern in our WebSocket Hibernation Guide.
Comparison with Existing Frameworks: LangGraph vs. Mastra
The Agents SDK is not the only choice. Comparing it with other popular agent frameworks clarifies its distinct positioning:
| Criteria | Cloudflare Agents SDK | LangGraph | Mastra |
|---|---|---|---|
| Execution Environment | Cloudflare Edge (Exclusive) | Anywhere (Cloud, Local) | Anywhere (Node.js) |
| State Management | Durable Objects Built-in SQLite | External Checkpointer (Redis, Postgres) | External DB Required |
| Real-time Communication | Native WebSockets + React Hooks | Custom implementation or LangServe | Custom implementation |
| Long-running Workflows | Hibernation + Workflows | AsyncIO + External Queues | External Queues Required |
| Deployment | One-command wrangler deploy |
Custom container/serverless setup | Custom container/serverless setup |
| Vendor Lock-in | High (Cloudflare exclusive) | Low | Low |
| Key Strengths | Zero infra, global edge, cost-efficient | Graph-based complex workflows, flexibility | Clean API, rapid prototyping |
The core trade-off of the Agents SDK is eliminating infrastructure complexity in exchange for full Cloudflare lock-in. If your team already relies heavily on Cloudflare Workers, the Agents SDK provides the fastest path to production. Conversely, if you require a multi-cloud architecture or complex cyclic DAG workflows, LangGraph remains a better fit.
Frequently Asked Questions
How does the Agents SDK differ from Workers AI?
Workers AI is a model inference API that runs AI models like Llama and Whisper on the edge. The Agents SDK is an agent framework that provides infrastructure for state management, tool calling, user interactions, and scheduling—the “brain” of the agent. In practice, you call Workers AI as an inference backend from within the Agents SDK.
Can multiple users connect to a single agent?
Yes. Durable Objects can handle multiple simultaneous WebSocket connections. By using an agent ID as a room ID, multiple users can connect to the same agent instance and share state in real time. However, due to the single-threaded nature of Durable Objects, scenarios with extremely high concurrent write throughput (thousands per second) may hit performance limits.
Can I add the Agents SDK to an existing Workers project?
Yes. Simply install the agents package, add Durable Objects bindings to wrangler.jsonc, and invoke routeAgentRequest(request, env) inside your Worker’s fetch handler. This allows standard HTTP routes and agent routing to co-exist, enabling incremental adoption without disrupting existing API endpoints.
How do I handle local development?
Running wrangler dev starts a local development server that emulates Durable Objects, SQLite, and WebSockets locally. You get the exact same API surface as production locally, allowing fast iteration without external cloud dependencies.