Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Builds AI agents on Cloudflare using the Agents SDK with state management, real-time WebSockets, scheduled tasks, tool integration, and chat capabilities. Generates production-ready agent code deployed to Workers. Use when: user wants to \"build an agent\", \"AI agent\", \"chat agent\", \"stateful agent\", mentions \"Agents SDK\", needs \"real-time AI\", \"WebSocket AI\", or asks about agent \"st
.claude/skills/kunanonj-cursor-plugin-cf-building-ai-agent-on-cloudflare/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 52% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 44% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 265% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 14% | 0% |
Creates AI-powered agents using Cloudflare's Agents SDK with persistent state, real-time communication, and tool integration.
npm install -g wrangler)bashnpm create cloudflare@latest -- my-agent --template=cloudflare/agents-starter cd my-agent npm start
Agent runs at http://localhost:8787
An Agent is a stateful, persistent AI service that:
Client connects → Agent.onConnect() → Agent processes messages
→ Agent.onMessage()
→ Agent.setState() (persists + syncs)
Client disconnects → State persists → Client reconnects → State restoredtypescriptimport { Agent, Connection } from "agents"; interface Env { AI: Ai; // Workers AI binding } interface State { messages: Array<{ role: string; content: string }>; preferences: Record<string, string>; } export class MyAgent extends Agent<Env, State> { // Initial state for new instances initialState: State = { messages: [], preferences: {}, }; // Called when agent starts or resumes async onStart() { console.log("Agent started with state:", this.state); } // Handle WebSocket connections async onConnect(connection: Connection) { connection.send(JSON.stringify({ type: "welcome", history: this.state.messages, })); } // Handle incoming messages async onMessage(connection: Connection, message: string) { const data = JSON.parse(message); if (data.type === "chat") { await this.handleChat(connection, data.content); } } // Handle disconnections async onClose(connection: Connection) { console.log("Client disconnected"); } // React to state changes onStateUpdate(state: State, source: string) { console.log("State updated by:", source); } private async handleChat(connection: Connection, userMessage: string) { // Add user message to history const messages = [ ...this.state.messages, { role: "user", content: userMessage }, ]; // Call AI const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", { messages, }); // Update state (persists and syncs to all clients) this.setState({ ...this.state, messages: [ ...messages, { role: "assistant", content: response.response }, ], }); // Send response connection.send(JSON.stringify({ type: "response", content: response.response, })); } }
typescript// src/index.ts import { routeAgentRequest } from "agents"; import { MyAgent } from "./agent"; export default { async fetch(request: Request, env: Env) { // routeAgentRequest handles routing to /agents/:class/:name return ( (await routeAgentRequest(request, env)) || new Response("Not found", { status: 404 }) ); }, }; export { MyAgent };
Clients connect via: wss://my-agent.workers.dev/agents/MyAgent/session-id
tomlname = "my-agent" main = "src/index.ts" compatibility_date = "2024-12-01" [ai] binding = "AI" [durable_objects] bindings = [{ name = "AGENT", class_name = "MyAgent" }] [[migrations]] tag = "v1" new_classes = ["MyAgent"]
typescript// Current state is always available const currentMessages = this.state.messages; const userPrefs = this.state.preferences;
typescript// setState persists AND syncs to all connected clients this.setState({ ...this.state, messages: [...this.state.messages, newMessage], }); // Partial updates work too this.setState({ preferences: { ...this.state.preferences, theme: "dark" }, });
For complex queries, use the embedded SQLite database:
typescript// Create tables await this.sql` CREATE TABLE IF NOT EXISTS documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `; // Insert await this.sql` INSERT INTO documents (title, content) VALUES (${title}, ${content}) `; // Query const docs = await this.sql` SELECT * FROM documents WHERE title LIKE ${`%${search}%`} `;
Agents can schedule future work:
typescriptasync onMessage(connection: Connection, message: string) { const data = JSON.parse(message); if (data.type === "schedule_reminder") { // Schedule task for 1 hour from now const { id } = await this.schedule(3600, "sendReminder", { message: data.reminderText, userId: data.userId, }); connection.send(JSON.stringify({ type: "scheduled", taskId: id })); } } // Called when scheduled task fires async sendReminder(data: { message: string; userId: string }) { // Send notification, email, etc. console.log(`Reminder for ${data.userId}: ${data.message}`); // Can also update state this.setState({ ...this.state, lastReminder: new Date().toISOString(), }); }
typescript// Delay in seconds await this.schedule(60, "taskMethod", { data }); // Specific date await this.schedule(new Date("2025-01-01T00:00:00Z"), "taskMethod", { data }); // Cron expression (recurring) await this.schedule("0 9 * * *", "dailyTask", {}); // 9 AM daily await this.schedule("*/5 * * * *", "everyFiveMinutes", {}); // Every 5 min // Manage schedules const schedules = await this.getSchedules(); await this.cancelSchedule(taskId);
For chat-focused agents, extend AIChatAgent:
typescriptimport { AIChatAgent } from "agents/ai-chat-agent"; export class ChatBot extends AIChatAgent<Env> { // Called for each user message async onChatMessage(message: string) { const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", { messages: [ { role: "system", content: "You are a helpful assistant." }, ...this.messages, // Automatic history management { role: "user", content: message }, ], stream: true, }); // Stream response back to client return response; } }
Features included:
saveMessages() for persistencetsximport { useAgent } from "agents/react"; function Chat() { const { state, send, connected } = useAgent({ agent: "my-agent", name: userId, // Agent instance ID }); const sendMessage = (text: string) => { send(JSON.stringify({ type: "chat", content: text })); }; return ( <div> {state.messages.map((msg, i) => ( <div key={i}>{msg.role}: {msg.content}</div> ))} <input onKeyDown={(e) => e.key === "Enter" && sendMessage(e.target.value)} /> </div> ); }
javascriptconst ws = new WebSocket("wss://my-agent.workers.dev/agents/MyAgent/user123"); ws.onopen = () => { console.log("Connected to agent"); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); console.log("Received:", data); }; ws.send(JSON.stringify({ type: "chat", content: "Hello!" }));
See references/agent-patterns.md for:
bash# Deploy npx wrangler deploy # View logs wrangler tail # Test endpoint curl https://my-agent.workers.dev/agents/MyAgent/test-user
See references/troubleshooting.md for common issues.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | pass→pass | 13,937 | 6,353 | -54% | 1 | 1 | 0% | 2,807 | 3,764 | +34% | 0 | 0 | — |
case-11 | fail→pass | 10,982 | 4,907 | -55% | 1 | 1 | 0% | 2,323 | 3,539 | +52% | 0 | 0 | — |
case-12 | pass→pass | 5,830 | 2,431 | -58% | 1 | 1 | 0% | 1,243 | 2,901 | +133% | 0 | 0 | — |
case-01 | fail→pass | 17,016 | 13,111 | -23% | 1 | 1 | 0% | 3,996 | 5,743 | +44% | 0 | 0 | — |
case-02 | fail→pass | 14,498 | 9,421 | -35% | 1 | 1 | 0% | 2,916 | 4,573 | +57% | 0 | 0 | — |
case-03 | pass→pass | 9,160 | 3,459 | -62% | 1 | 1 | 0% | 1,976 | 3,081 | +56% | 0 | 0 | — |
case-09 | pass→pass | 9,879 | 2,401 | -76% | 1 | 1 | 0% | 1,913 | 2,853 | +49% | 0 | 0 | — |
case-04 | fail→pass | 4,625 | 2,181 | -53% | 1 | 1 | 0% | 796 | 2,902 | +265% | 0 | 0 | — |
case-05 | fail→pass | 15,796 | 6,775 | -57% | 1 | 1 | 0% | 3,481 | 3,955 | +14% | 0 | 0 | — |
case-06 | fail→pass | 14,554 | 8,978 | -38% | 1 | 1 | 0% | 2,500 | 4,426 | +77% | 0 | 0 | — |
case-07 | fail→pass | 16,789 | 5,848 | -65% | 1 | 1 | 0% | 3,102 | 3,715 | +20% | 0 | 0 | — |
case-08 | fail→pass | 11,109 | 4,720 | -58% | 1 | 1 | 0% | 2,402 | 3,344 | +39% | 0 | 0 | — |
case-13 | fail→pass | 19,735 | 1,807 | -91% | 1 | 1 | 0% | 2,918 | 2,786 | -5% | 0 | 0 | — |
case-14 | fail→pass | 3,712 | 1,334 | -64% | 1 | 1 | 0% | 740 | 2,654 | +259% | 0 | 0 | — |
case-15 | fail→pass | 7,861 | 2,183 | -72% | 1 | 1 | 0% | 1,636 | 2,908 | +78% | 0 | 0 | — |
case-16 | pass→pass | 10,108 | 5,664 | -44% | 1 | 1 | 0% | 2,027 | 3,682 | +82% | 0 | 0 | — |
case-17 | fail→pass | 9,294 | 2,571 | -72% | 1 | 1 | 0% | 1,890 | 2,871 | +52% | 0 | 0 | — |
case-18 | pass→pass | 3,659 | 1,771 | -52% | 1 | 1 | 0% | 843 | 2,736 | +225% | 0 | 0 | — |
case-19 | pass→pass | 6,453 | 1,745 | -73% | 1 | 1 | 0% | 1,372 | 2,746 | +100% | 0 | 0 | — |
case-20 | fail→fail | 11,139 | 8,885 | -20% | 1 | 1 | 0% | 2,739 | 4,636 | +69% | 0 | 0 | — |
case-21 | pass→pass | 8,336 | 5,212 | -37% | 1 | 1 | 0% | 1,842 | 3,551 | +93% | 0 | 0 | — |
case-22 | pass→pass | 8,611 | 5,499 | -36% | 1 | 1 | 0% | 2,093 | 3,748 | +79% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +55 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.