Loading...
Loading...
Build AI agents with Cloudflare Agents SDK on Workers + Durable Objects. Includes critical guidance on choosing between Agents SDK (infrastructure/state) vs AI SDK (simpler flows). Use when: deciding SDK choice, building WebSocket agents with state, RAG with Vectorize, MCP servers, multi-agent orchestration, or troubleshooting "Agent class must extend", "new_sqlite_classes", binding errors.
npx skill4agent add ovachiever/droid-tings cloudflare-agentsimport { context }// worker.ts - Simple chat with AI SDK only
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export default {
async fetch(request: Request, env: Env) {
const { messages } = await request.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages
});
return result.toTextStreamResponse(); // Automatic SSE streaming
}
}
// client.tsx - React with built-in hooks
import { useChat } from 'ai/react';
function ChatPage() {
const { messages, input, handleSubmit } = useChat({ api: '/api/chat' });
// Done. No Agents SDK needed.
}Building an AI application?
│
├─ Need WebSocket bidirectional communication? ───────┐
│ (Client sends while server streams, agent-initiated messages)
│
├─ Need Durable Objects stateful instances? ──────────┤
│ (Globally unique agents with persistent memory)
│
├─ Need multi-agent coordination? ────────────────────┤
│ (Agents calling/messaging other agents)
│
├─ Need scheduled tasks or cron jobs? ────────────────┤
│ (Delayed execution, recurring tasks)
│
├─ Need human-in-the-loop workflows? ─────────────────┤
│ (Approval gates, review processes)
│
└─ If ALL above are NO ─────────────────────────────→ Use AI SDK directly
(Much simpler approach)
If ANY above are YES ────────────────────────────→ Use Agents SDK + AI SDK
(More infrastructure, more power)| Feature | AI SDK Only | Agents SDK + AI SDK |
|---|---|---|
| Setup Complexity | 🟢 Low (npm install, done) | 🔴 Higher (Durable Objects, migrations, bindings) |
| Code Volume | 🟢 ~100 lines | 🟡 ~500+ lines |
| Streaming | ✅ Automatic (SSE) | ✅ Automatic (AI SDK) or manual (Workers AI) |
| State Management | ⚠️ Manual (D1/KV) | ✅ Built-in (SQLite) |
| WebSockets | ❌ Manual setup | ✅ Built-in |
| React Hooks | ✅ useChat, useCompletion | ⚠️ Custom hooks needed |
| Multi-agent | ❌ Not supported | ✅ Built-in (routeAgentRequest) |
| Scheduling | ❌ External (Queue/Workflow) | ✅ Built-in (this.schedule) |
| Use Case | Simple chat, completions | Complex stateful workflows |
npm create cloudflare@latest my-agent -- \
--template=cloudflare/agents-starter \
--ts \
--git \
--deploy falsecd my-existing-worker
npm install agents// src/index.ts
import { Agent, AgentNamespace } from "agents";
export class MyAgent extends Agent {
async onRequest(request: Request): Promise<Response> {
return new Response("Hello from Agent!");
}
}
export default MyAgent;wrangler.jsonc{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-agent",
"main": "src/index.ts",
"compatibility_date": "2025-10-21",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "MyAgent", // MUST match class name
"class_name": "MyAgent" // MUST match exported class
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyAgent"] // CRITICAL: Enables SQLite storage
}
]
}nameclass_namenew_sqlite_classesnpx wrangler@latest deployhttps://my-agent.<subdomain>.workers.dev┌─────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ ┌────────────────┐ ┌──────────────────────┐ │
│ │ Agents SDK │ │ AI Inference │ │
│ │ (Infra Layer) │ + │ (Brain Layer) │ │
│ │ │ │ │ │
│ │ • WebSockets │ │ Choose ONE: │ │
│ │ • Durable Objs │ │ • Vercel AI SDK ✅ │ │
│ │ • State (SQL) │ │ • Workers AI ⚠️ │ │
│ │ • Scheduling │ │ • OpenAI Direct │ │
│ │ • Multi-agent │ │ • Anthropic Direct │ │
│ └────────────────┘ └──────────────────────┘ │
│ ↓ ↓ │
│ Manages connections Generates responses │
│ and state and handles streaming │
└─────────────────────────────────────────────────────────┘
↓
Cloudflare Workers + Durable ObjectsonStartonConnectonMessageonClosethis.schedule()routeAgentRequest()useAgentAgentClientagentFetchuseChatuseCompletionuseAssistantimport { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = streamText({
model: openai('gpt-4o-mini'),
messages: [...]
});
// Returns SSE stream - no manual parsing needed
return result.toTextStreamResponse();import { AIChatAgent } from "agents/ai-chat-agent";
import { streamText } from "ai";
export class MyAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
// Agents SDK provides: WebSocket, state, this.messages
// AI SDK provides: Automatic streaming, provider abstraction
return streamText({
model: openai('gpt-4o-mini'),
messages: this.messages // Managed by Agents SDK
}).toTextStreamResponse();
}
}const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
messages: [...],
stream: true
});
// Returns raw SSE format - YOU must parse
for await (const chunk of response) {
const text = new TextDecoder().decode(chunk); // Uint8Array → string
if (text.startsWith('data: ')) { // Check SSE format
const data = JSON.parse(text.slice(6)); // Parse JSON
if (data.response) { // Extract .response field
fullResponse += data.response;
}
}
}import { AIChatAgent } from "agents/ai-chat-agent";
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return streamText({
model: openai('gpt-4o-mini'),
messages: this.messages, // Agents SDK manages history
onFinish
}).toTextStreamResponse();
}
}import { Agent } from "agents";
export class BudgetAgent extends Agent<Env> {
async onMessage(connection, message) {
const response = await this.env.AI.run('@cf/meta/llama-3-8b-instruct', {
messages: [...],
stream: true
});
// Manual SSE parsing required (see Workers AI section above)
for await (const chunk of response) {
// ... manual parsing ...
}
}
}// worker.ts - Simple Workers route
export default {
async fetch(request: Request, env: Env) {
const { messages } = await request.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages
});
return result.toTextStreamResponse();
}
}
// client.tsx - Built-in React hooks
import { useChat } from 'ai/react';
function Chat() {
const { messages, input, handleSubmit } = useChat({ api: '/api/chat' });
return <form onSubmit={handleSubmit}>...</form>;
}| Your Needs | Recommended Stack | Complexity | Cost |
|---|---|---|---|
| Simple chat, no state | AI SDK only | 🟢 Low | $$ (AI provider) |
| Chat + WebSockets + state | Agents SDK + AI SDK | 🟡 Medium | $$$ (infra + AI) |
| Chat + WebSockets + budget | Agents SDK + Workers AI | 🔴 High | $ (infra only) |
| Multi-agent workflows | Agents SDK + AI SDK | 🔴 High | $$$ (infra + AI) |
| MCP server with tools | Agents SDK (McpAgent) | 🟡 Medium | $ (infra only) |
ai-sdk-corecloudflare-workers-ai{
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] } // MUST be in first migration
]
}aivectorizebrowserworkflowsd1_databasesr2_bucketsnew_sqlite_classesnameclass_nameAgent<Env, State>onStart()onRequest()onConnect/onMessage/onClose()onStateUpdate()this.envthis.statethis.setState()this.sqlthis.namethis.schedule()export class ChatAgent extends Agent<Env, State> {
async onConnect(connection: Connection, ctx: ConnectionContext) {
// Auth check, add to participants, send welcome
}
async onMessage(connection: Connection, message: WSMessage) {
// Process message, update state, broadcast response
}
}this.setState(newState)this.sqlawait this.sql`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT)`
await this.sql`INSERT INTO users (email) VALUES (${userEmail})` // ← Prepared statement
const users = await this.sql`SELECT * FROM users WHERE email = ${email}` // ← Returns arraythis.schedule()export class MyAgent extends Agent {
async onRequest(request: Request): Promise<Response> {
// Schedule task to run in 60 seconds
const { id } = await this.schedule(60, "checkStatus", { requestId: "123" });
return Response.json({ scheduledTaskId: id });
}
// This method will be called in 60 seconds
async checkStatus(data: { requestId: string }) {
console.log('Checking status for request:', data.requestId);
// Perform check, update state, send notification, etc.
}
}export class MyAgent extends Agent {
async scheduleReminder(reminderDate: string) {
const date = new Date(reminderDate);
const { id } = await this.schedule(date, "sendReminder", {
message: "Time for your appointment!"
});
return id;
}
async sendReminder(data: { message: string }) {
console.log('Sending reminder:', data.message);
// Send email, push notification, etc.
}
}export class MyAgent extends Agent {
async setupRecurringTasks() {
// Every 10 minutes
await this.schedule("*/10 * * * *", "checkUpdates", {});
// Every day at 8 AM
await this.schedule("0 8 * * *", "dailyReport", {});
// Every Monday at 9 AM
await this.schedule("0 9 * * 1", "weeklyReport", {});
// Every hour on the hour
await this.schedule("0 * * * *", "hourlyCheck", {});
}
async checkUpdates(data: any) {
console.log('Checking for updates...');
}
async dailyReport(data: any) {
console.log('Generating daily report...');
}
async weeklyReport(data: any) {
console.log('Generating weekly report...');
}
async hourlyCheck(data: any) {
console.log('Running hourly check...');
}
}export class MyAgent extends Agent {
async manageSchedules() {
// Get all scheduled tasks
const allTasks = this.getSchedules();
console.log('Total tasks:', allTasks.length);
// Get specific task by ID
const taskId = "some-task-id";
const task = await this.getSchedule(taskId);
if (task) {
console.log('Task:', task.callback, 'at', new Date(task.time));
console.log('Payload:', task.payload);
console.log('Type:', task.type); // "scheduled" | "delayed" | "cron"
// Cancel the task
const cancelled = await this.cancelSchedule(taskId);
console.log('Cancelled:', cancelled);
}
// Get tasks in time range
const upcomingTasks = this.getSchedules({
timeRange: {
start: new Date(),
end: new Date(Date.now() + 24 * 60 * 60 * 1000) // Next 24 hours
}
});
console.log('Upcoming tasks:', upcomingTasks.length);
// Filter by type
const cronTasks = this.getSchedules({ type: "cron" });
const delayedTasks = this.getSchedules({ type: "delayed" });
}
}(task_size * count) + other_state < 1GB// ❌ BAD: Method doesn't exist
await this.schedule(60, "nonExistentMethod", {});
// ✅ GOOD: Method exists
await this.schedule(60, "existingMethod", {});
async existingMethod(data: any) {
// Implementation
}wrangler.jsonc{
"workflows": [
{
"name": "MY_WORKFLOW",
"class_name": "MyWorkflow"
}
]
}{
"workflows": [
{
"name": "EMAIL_WORKFLOW",
"class_name": "EmailWorkflow",
"script_name": "email-workflows" // Different project
}
]
}import { Agent } from "agents";
import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers";
interface Env {
MY_WORKFLOW: Workflow;
MyAgent: AgentNamespace<MyAgent>;
}
export class MyAgent extends Agent<Env> {
async onRequest(request: Request): Promise<Response> {
const userId = new URL(request.url).searchParams.get('userId');
// Trigger a workflow immediately
const instance = await this.env.MY_WORKFLOW.create({
id: `user-${userId}`,
params: { userId, action: "process" }
});
// Or schedule a delayed workflow trigger
await this.schedule(300, "runWorkflow", { userId });
return Response.json({ workflowId: instance.id });
}
async runWorkflow(data: { userId: string }) {
const instance = await this.env.MY_WORKFLOW.create({
id: `delayed-${data.userId}`,
params: data
});
// Monitor workflow status periodically
await this.schedule("*/5 * * * *", "checkWorkflowStatus", { id: instance.id });
}
async checkWorkflowStatus(data: { id: string }) {
// Check workflow status (see Workflows docs for details)
console.log('Checking workflow:', data.id);
}
}
// Workflow definition (can be in same or different file/project)
export class MyWorkflow extends WorkflowEntrypoint<Env> {
async run(event: WorkflowEvent<{ userId: string }>, step: WorkflowStep) {
// Workflow implementation
const result = await step.do('process-data', async () => {
return { processed: true };
});
return result;
}
}| Feature | Agents | Workflows |
|---|---|---|
| Purpose | Interactive, user-facing | Background processing |
| Duration | Seconds to hours | Minutes to hours |
| State | SQLite database | Step-based checkpoints |
| Interaction | WebSockets, HTTP | No direct interaction |
| Retry | Manual | Automatic per step |
| Use Case | Chat, real-time UI | ETL, batch processing |
"browser": { "binding": "BROWSER" }@cloudflare/puppeteercloudflare-browser-rendering"ai": { "binding": "AI" }"vectorize": { "bindings": [{ "binding": "VECTORIZE", "index_name": "my-vectors" }] }@cf/baai/bge-base-en-v1.5this.env.VECTORIZE.upsert(vectors)this.env.VECTORIZE.query(queryVector, { topK: 5 })cloudflare-vectorizeai-sdk-corecloudflare-workers-airouteAgentRequest(request, env)/agents/:agent/:name/agents/my-agent/user-123getAgentByName<Env, T>(env.AgentBinding, instanceName)const agent = getAgentByName(env.MyAgent, 'user-${userId}')export class AgentA extends Agent<Env> {
async processData(data: any) {
const agentB = getAgentByName<Env, AgentB>(this.env.AgentB, 'processor-1');
return await (await agentB).analyze(data);
}
}AgentClientagents/clientagentFetchagents/clientuseAgentagents/reactuseAgentChatagents/ai-reactnpm install @modelcontextprotocol/sdk agentsimport { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new McpServer({ name: "Demo", version: "1.0.0" });
async init() {
// Define a tool
this.server.tool(
"add",
"Add two numbers together",
{
a: z.number().describe("First number"),
b: z.number().describe("Second number")
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
}
}type State = { counter: number };
export class StatefulMCP extends McpAgent<Env, State> {
server = new McpServer({ name: "Counter", version: "1.0.0" });
initialState: State = { counter: 0 };
async init() {
// Resource
this.server.resource(
"counter",
"mcp://resource/counter",
(uri) => ({
contents: [{ uri: uri.href, text: String(this.state.counter) }]
})
);
// Tool
this.server.tool(
"increment",
"Increment the counter",
{ amount: z.number() },
async ({ amount }) => {
this.setState({
...this.state,
counter: this.state.counter + amount
});
return {
content: [{
type: "text",
text: `Counter is now ${this.state.counter}`
}]
};
}
);
}
}import { Hono } from 'hono';
const app = new Hono();
// Modern streamable HTTP transport (recommended)
app.mount('/mcp', MyMCP.serve('/mcp').fetch, { replaceRequest: false });
// Legacy SSE transport (deprecated)
app.mount('/sse', MyMCP.serveSSE('/sse').fetch, { replaceRequest: false });
export default app;import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
export default new OAuthProvider({
apiHandlers: {
'/sse': MyMCP.serveSSE('/sse'),
'/mcp': MyMCP.serve('/mcp')
},
// OAuth configuration
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
// ... other OAuth settings
});# Run MCP inspector
npx @modelcontextprotocol/inspector@latest
# Connect to: http://localhost:8788/mcpnpx wrangler versions deploynew_sqlite_classesexport class MyAgent extends Agentnameclass_name"browser": { "binding": "BROWSER" }wrangler vectorize create/mcpMyMCP.serve('/mcp')/sseuser-${userId}Uint8ArrayTextDecoderconst response = await env.AI.run(model, { stream: true });
for await (const chunk of response) {
console.log(chunk.response); // ❌ undefined - chunk is Uint8Array, not object
}const response = await env.AI.run(model, { stream: true });
for await (const chunk of response) {
const text = new TextDecoder().decode(chunk); // Step 1: Uint8Array → string
if (text.startsWith('data: ')) { // Step 2: Check SSE format
const jsonStr = text.slice(6).trim(); // Step 3: Extract JSON from "data: {...}"
if (jsonStr === '[DONE]') break; // Step 4: Handle termination
const data = JSON.parse(jsonStr); // Step 5: Parse JSON
if (data.response) { // Step 6: Extract .response field
fullResponse += data.response;
}
}
}import { streamText } from 'ai';
import { createCloudflare } from '@ai-sdk/cloudflare';
const cloudflare = createCloudflare();
const result = streamText({
model: cloudflare('@cf/meta/llama-3-8b-instruct', { binding: env.AI }),
messages
});
// No manual parsing needed ✅agents@modelcontextprotocol/sdk@cloudflare/puppeteerai@ai-sdk/openai@ai-sdk/anthropicwrangler-agents-config.jsoncbasic-agent.tswebsocket-agent.tsstate-sync-agent.tsscheduled-agent.tsworkflow-agent.tsbrowser-agent.tsrag-agent.tschat-agent-streaming.tscalling-agents-worker.tsreact-useagent-client.tsxmcp-server-basic.tshitl-agent.tsagent-class-api.mdclient-api-reference.mdstate-management-guide.mdwebsockets-sse.mdscheduling-api.mdworkflows-integration.mdbrowser-rendering.mdrag-patterns.mdmcp-server-guide.mdmcp-tools-reference.mdhitl-patterns.mdbest-practices.mdchat-bot-complete.mdmulti-agent-workflow.mdscheduled-reports.mdbrowser-scraper-agent.mdrag-knowledge-base.mdmcp-remote-server.md