skills-templates/cloudflare-agents/SKILL.md
Cloudflare Agents SDK - Build and deploy AI-powered agents on Cloudflare's edge with real-time WebSocket communication, persistent state, SQL storage, task scheduling, MCP integration, and human-in-the-loop workflows.
npx skillsauth add enuno/claude-command-and-control cloudflare-agentsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The Cloudflare Agents SDK enables you to build and deploy AI-powered agents that run on Cloudflare's global edge network. Agents can autonomously perform tasks, communicate with clients in real-time via WebSockets, call AI models, persist state, schedule tasks, run asynchronous workflows, browse the web, query data from databases, support human-in-the-loop interactions, and act as MCP (Model Context Protocol) servers or clients.
Core Value: Agents deploy on Cloudflare's Durable Objects - stateful micro-servers that can scale to tens of millions and run close to users for low-latency interactivity.
This skill should be triggered when:
# Add to existing Workers project
npm i agents
# Or create new project from starter template
npm create cloudflare@latest agents-starter -- --template=cloudflare/agents-starter
# Navigate and run
cd agents-starter
mv .env.local .env
npm install
npm run dev
// Core Agent classes
import { Agent } from "agents";
import { AIChatAgent } from "agents/ai-chat-agent";
import { McpAgent } from "agents/mcp";
// Client-side
import { AgentClient } from "agents/client";
// React hooks
import { useAgent } from "agents/react";
import { useAgentChat } from "agents/ai-react";
// Routing
import { routeAgentRequest, getAgentByName } from "agents";
import { Agent, routeAgentRequest } from "agents";
// Define your Agent class
export class MyAgent extends Agent<Env, State> {
// Optional: Set initial state
initialState = {
counter: 0,
messages: [],
};
// Called when agent starts or resumes
async onStart() {
console.log("Agent started");
}
// Handle HTTP requests
async onRequest(request: Request): Promise<Response> {
return new Response("Hello from Agent!");
}
// Handle WebSocket connections
async onConnect(connection: Connection, ctx: ConnectionContext) {
console.log(`Client connected: ${connection.id}`);
}
// Handle WebSocket messages
async onMessage(connection: Connection, message: WSMessage) {
const data = JSON.parse(message as string);
connection.send(JSON.stringify({ received: data }));
}
// Handle state updates
onStateUpdate(state: State, source: "server" | Connection) {
console.log("State updated:", state);
}
}
// Export default handler with routing
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return routeAgentRequest(request, env);
},
};
{
"name": "my-agent",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"observability": {
"enabled": true
},
"durable_objects": {
"bindings": [
{
"name": "MY_AGENT",
"class_name": "MyAgent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyAgent"]
}
]
}
The base class for all agents, providing lifecycle hooks and capabilities.
class Agent<Env, State> {
// Properties
env: Env; // Environment bindings
state: State; // Current state (read-only)
initialState?: State; // Optional default state
// Lifecycle hooks
onStart(): void | Promise<void>;
onRequest(request: Request): Response | Promise<Response>;
onConnect(connection: Connection, ctx: ConnectionContext): void;
onMessage(connection: Connection, message: WSMessage): void;
onError(connection: Connection, error: Error): void;
onClose(connection: Connection, code: number, reason: string, wasClean: boolean): void;
onStateUpdate(state: State, source: "server" | Connection): void;
// State management
setState(state: Partial<State>): void;
// SQL database
sql`query`: SQLResult;
// Task scheduling
schedule(when: number | Date | string, callback: string, data?: any): string;
getSchedule(id: string): Schedule | undefined;
getSchedules(criteria?: ScheduleCriteria): Schedule[];
cancelSchedule(id: string): void;
// MCP integration
addMcpServer(name: string, url: string, ...): void;
removeMcpServer(id: string): void;
getMcpServers(): McpServer[];
}
Extended Agent class for building chat interfaces with AI models.
import { AIChatAgent } from "agents/ai-chat-agent";
import { createOpenAI } from "@ai-sdk/openai";
import { streamText, createDataStreamResponse } from "ai";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish: (result: any) => void) {
return createDataStreamResponse({
execute: async (dataStream) => {
const openai = createOpenAI({
apiKey: this.env.OPENAI_API_KEY,
});
const stream = streamText({
model: openai("gpt-4o"),
messages: this.messages, // Built-in message history
onFinish,
});
stream.mergeIntoDataStream(dataStream);
},
});
}
}
Features:
this.messagesinterface GameState {
players: string[];
score: number;
status: "waiting" | "playing" | "finished";
}
class GameAgent extends Agent<Env, GameState> {
initialState: GameState = {
players: [],
score: 0,
status: "waiting",
};
async addPlayer(name: string) {
this.setState({
...this.state,
players: [...this.state.players, name],
});
}
async startGame() {
this.setState({
...this.state,
status: "playing",
});
}
onStateUpdate(state: GameState, source: "server" | Connection) {
// Triggered on every state change
// source indicates if change was from server or a client
console.log(`State updated by ${source}:`, state);
}
}
import { useAgent } from "agents/react";
import { useState, useEffect } from "react";
function GameInterface() {
const [state, setState] = useState({ players: [], score: 0 });
const agent = useAgent({
agent: "game-agent",
name: "room-123", // Unique instance identifier
onStateUpdate: (newState) => setState(newState),
});
const addPlayer = (name: string) => {
agent.setState({
...state,
players: [...state.players, name],
});
};
return (
<div>
<h2>Players: {state.players.join(", ")}</h2>
<button onClick={() => addPlayer("Player")}>Join</button>
</div>
);
}
State Characteristics:
Each Agent has an embedded SQLite database with zero-latency access.
class DataAgent extends Agent<Env, State> {
// Basic query
async getUser(userId: string) {
const [user] = this.sql`SELECT * FROM users WHERE id = ${userId}`;
return user;
}
// Typed query
async getUsers(): User[] {
return this.sql<User>`SELECT * FROM users ORDER BY created_at DESC`;
}
// Insert data
async createUser(name: string, email: string) {
this.sql`INSERT INTO users (name, email, created_at)
VALUES (${name}, ${email}, ${new Date().toISOString()})`;
}
// Transaction-like operations
async transferCredits(from: string, to: string, amount: number) {
this.sql`UPDATE users SET credits = credits - ${amount} WHERE id = ${from}`;
this.sql`UPDATE users SET credits = credits + ${amount} WHERE id = ${to}`;
}
}
Database Limits:
Schedule tasks for future execution using delays, dates, or cron expressions.
class SchedulerAgent extends Agent<Env, State> {
async scheduleReminder(userId: string, message: string, when: Date) {
// Schedule with Date
const taskId = this.schedule(when, "sendReminder", { userId, message });
return taskId;
}
async scheduleDelay(seconds: number) {
// Schedule with delay in seconds
this.schedule(seconds, "delayedTask", { data: "payload" });
}
async scheduleCron() {
// Schedule with cron expression (runs daily at 9am UTC)
this.schedule("0 9 * * *", "dailyReport", {});
}
// Callback methods (must match callback string)
async sendReminder(data: { userId: string; message: string }) {
// Send notification to user
console.log(`Reminder for ${data.userId}: ${data.message}`);
}
async delayedTask(data: { data: string }) {
console.log("Delayed task executed:", data);
}
async dailyReport() {
// Generate daily report
}
// Query scheduled tasks
async listSchedules() {
const all = this.getSchedules();
const pending = this.getSchedules({ type: "scheduled" });
const crons = this.getSchedules({ type: "cron" });
return { all, pending, crons };
}
// Cancel a task
async cancelTask(taskId: string) {
this.cancelSchedule(taskId);
}
}
Schedule Types:
"scheduled" - One-time at specific Date"delayed" - After N seconds"cron" - Recurring via cron expressionclass ChatRoomAgent extends Agent<Env, State> {
onConnect(connection: Connection, ctx: ConnectionContext) {
// Access request headers, cookies, URL
const url = new URL(ctx.request.url);
const token = url.searchParams.get("token");
// Validate and accept connection
if (!this.validateToken(token)) {
connection.close(4001, "Unauthorized");
return;
}
// Store connection-specific state
connection.setState({ userId: token });
// Send welcome message
connection.send(JSON.stringify({ type: "welcome", state: this.state }));
}
onMessage(connection: Connection, message: WSMessage) {
const data = JSON.parse(message as string);
switch (data.type) {
case "chat":
this.broadcastMessage(connection, data.content);
break;
case "typing":
this.broadcastTyping(connection);
break;
}
}
onClose(connection: Connection, code: number, reason: string, wasClean: boolean) {
console.log(`Client ${connection.id} disconnected: ${reason}`);
}
onError(connection: Connection, error: Error) {
console.error(`Connection error: ${error.message}`);
}
private broadcastMessage(sender: Connection, content: string) {
// Broadcast to all connections (accessed via this mechanism)
// Implementation depends on tracking connections
}
}
import { AgentClient } from "agents/client";
const client = new AgentClient({
agent: "chat-room-agent",
name: "room-general",
});
client.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
console.log("Received:", data);
});
client.addEventListener("open", () => {
client.send(JSON.stringify({ type: "chat", content: "Hello!" }));
});
client.addEventListener("close", (event) => {
console.log("Disconnected:", event.reason);
});
import { useAgent } from "agents/react";
function ChatRoom() {
const [messages, setMessages] = useState([]);
const agent = useAgent({
agent: "chat-room-agent",
name: "room-general",
onMessage: (message) => {
const data = JSON.parse(message.data);
setMessages((prev) => [...prev, data]);
},
onOpen: () => console.log("Connected"),
onClose: () => console.log("Disconnected"),
});
const sendMessage = (content: string) => {
agent.send(JSON.stringify({ type: "chat", content }));
};
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.content}</div>
))}
<input onKeyDown={(e) => {
if (e.key === "Enter") sendMessage(e.target.value);
}} />
</div>
);
}
Build complete chat interfaces with the useAgentChat hook.
import { useAgent } from "agents/react";
import { useAgentChat } from "agents/ai-react";
function AIChat() {
const agent = useAgent({
agent: "chat-agent",
name: "session-123",
});
const {
messages,
input,
handleInputChange,
handleSubmit,
isLoading,
clearHistory,
} = useAgentChat({
agent,
maxSteps: 10, // Max tool invocations per message
});
return (
<div className="chat-container">
<div className="messages">
{messages.map((msg) => (
<div key={msg.id} className={msg.role}>
{msg.content}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
<button onClick={clearHistory}>Clear History</button>
</div>
);
}
Implement approval workflows for sensitive AI tool executions.
import { AIChatAgent } from "agents/ai-chat-agent";
import { streamText, tool } from "ai";
// Tools requiring confirmation
const toolsRequiringConfirmation = ["send_email", "make_payment", "delete_data"];
export class ApprovalAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return createDataStreamResponse({
execute: async (dataStream) => {
const stream = streamText({
model: openai("gpt-4o"),
messages: this.messages,
tools: {
send_email: tool({
description: "Send an email to a user",
parameters: z.object({
to: z.string(),
subject: z.string(),
body: z.string(),
}),
// No execute = requires confirmation
}),
get_weather: tool({
description: "Get weather data",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => {
// Auto-executes without confirmation
return { temp: 72, conditions: "sunny" };
},
}),
},
onFinish,
});
stream.mergeIntoDataStream(dataStream);
},
});
}
}
import { useAgentChat } from "agents/ai-react";
function ApprovalChat() {
const { messages, handleSubmit, addToolResult } = useAgentChat({
agent,
maxSteps: 10,
});
const handleApprove = async (toolCallId: string, args: any) => {
// Execute the tool and return result
const result = await executeToolOnServer(toolCallId, args);
addToolResult({ toolCallId, result });
};
const handleReject = (toolCallId: string) => {
addToolResult({ toolCallId, result: "User rejected this action" });
};
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
{msg.content}
{/* Show approval UI for pending tool calls */}
{msg.toolInvocations?.map((tool) => (
tool.state === "call" && (
<div key={tool.toolCallId} className="approval-prompt">
<p>Approve {tool.toolName}?</p>
<pre>{JSON.stringify(tool.args, null, 2)}</pre>
<button onClick={() => handleApprove(tool.toolCallId, tool.args)}>
Approve
</button>
<button onClick={() => handleReject(tool.toolCallId)}>
Reject
</button>
</div>
)
))}
</div>
))}
</div>
);
}
Best Practices:
Expose your Agent as an MCP server for AI assistants.
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export class MyMcpServer extends McpAgent<Env> {
server = new McpServer({
name: "my-mcp-server",
version: "1.0.0",
});
async init() {
// Register tools
this.server.tool(
"search_database",
"Search the database for records",
{ query: z.string() },
async (args) => {
const results = this.sql`SELECT * FROM records WHERE name LIKE ${args.query}`;
return { content: [{ type: "text", text: JSON.stringify(results) }] };
}
);
// Register resources
this.server.resource(
"database://records",
"Database records",
async () => {
const records = this.sql`SELECT * FROM records`;
return { content: [{ type: "text", text: JSON.stringify(records) }] };
}
);
}
}
Connect to external MCP servers from your Agent.
class AgentWithMcp extends Agent<Env, State> {
async onStart() {
// Connect to MCP server
await this.addMcpServer(
"weather-service",
"https://weather-mcp.example.com",
this.env.MCP_CALLBACK_HOST,
"/agents"
);
}
async getWeather(city: string) {
// MCP tools are automatically available
// Tools are namespaced by server ID
const servers = this.getMcpServers();
// Use tools via AI model integration
}
async cleanup() {
await this.removeMcpServer("weather-service");
}
}
MCP Features:
import { routeAgentRequest } from "agents";
export default {
async fetch(request: Request, env: Env) {
// Routes /agents/:agent/:name automatically
// e.g., /agents/my-agent/user-123
return routeAgentRequest(request, env);
},
};
import { getAgentByName } from "agents";
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/custom/")) {
const agentName = url.pathname.split("/")[2];
const agent = getAgentByName(env.MY_AGENT, agentName);
return agent.fetch(request);
}
return new Response("Not found", { status: 404 });
},
};
import { getAgentByName } from "agents";
export default {
async fetch(request: Request, env: Env) {
const agent = getAgentByName(env.MY_AGENT, "user-123");
// Call methods directly (no HTTP serialization)
const response = await agent.chat("Hello!");
const state = await agent.getState();
return Response.json({ response, state });
},
};
export default {
async fetch(request: Request, env: Env) {
return routeAgentRequest(request, env, {
onBeforeConnect: async (request) => {
const token = request.headers.get("Authorization");
if (!validateToken(token)) {
return new Response("Unauthorized", { status: 401 });
}
return null; // Continue to agent
},
onBeforeRequest: async (request) => {
// Similar validation for HTTP requests
},
});
},
};
import { Hono } from "hono";
import { jwt } from "hono/jwt";
import { getAgentByName } from "agents";
const app = new Hono();
app.use("/agents/*", jwt({ secret: process.env.JWT_SECRET }));
app.all("/agents/:agent/:name/*", async (c) => {
const user = c.get("jwtPayload");
const agent = getAgentByName(c.env.MY_AGENT, c.req.param("name"));
return agent.fetch(c.req.raw);
});
export default app;
# Deploy to Cloudflare
npm run deploy
# or
wrangler deploy
# Set secrets
wrangler secret put OPENAI_API_KEY
wrangler secret put JWT_SECRET
# View logs
wrangler tail
| Service | Integration |
|---------|-------------|
| Workers AI | Serverless GPU-powered models via @cloudflare/ai |
| AI Gateway | Caching, rate limiting, model fallbacks |
| Vectorize | Vector database for RAG and semantic search |
| Workflows | Stateful execution with automatic retries |
| D1 | Additional SQL database storage |
| R2 | Object storage for large files |
| KV | Key-value storage for config/cache |
This skill includes comprehensive documentation in references/:
tools
MemPalace local-first AI memory system. Use when setting up persistent memory for Claude Code sessions, mining project files or conversation transcripts, querying past context, configuring MCP tools, managing the knowledge graph, or troubleshooting palace operations.
tools
LangSmith Python SDK — trace, evaluate, and monitor LLM applications. Covers @traceable decorator, trace context manager, Client API, evaluate() / aevaluate(), comparative evaluation, custom evaluators, dataset management, prompt caching, ASGI middleware, and pytest plugin.
development
LangGraph (Python) — build stateful, controllable agent graphs with checkpointing, streaming, persistence, interrupts, fault tolerance, and durable execution. Covers both Graph API (StateGraph) and Functional API (@entrypoint/@task).
development
LangGraph Graph API (Python) — build explicit DAG agent workflows with StateGraph, typed state, nodes, edges, Command routing, Send fan-out, checkpointers, interrupts, and streaming. Use when you need explicit control flow and graph topology.