effidevFlutter · Cloudflare edge · Cloud cost optimization
English

Cloudflare Agents SDK: Building Stateful AI Agents on the Edge

Building stateful AI agents with Cloudflare Agents SDK

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 Agent class and exposing methods with @callable(), frontends can instantly make type-safe WebSocket RPC calls using the useAgent hook.
  • Human-in-the-Loop supports gating tool calls with the needsApproval flag 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:

  1. 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.

  2. Keep the 30-second CPU limit in mind. While LLM API calls are asynchronous await waits 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.

  3. 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.