A starter template for building AI chat agents on Cloudflare, powered by the Agents SDK.
Uses Workers AI (no API key required), with tools for weather, timezone detection, calculations with approval, and task scheduling.
npx create-cloudflare@latest --template cloudflare/agents-starter
cd agents-starter
npm install
npm run devOpen http://localhost:5173 to see your agent in action.
Try these prompts to see the different features:
- "What's the weather in Paris?" — server-side tool (runs automatically)
- "What timezone am I in?" — client-side tool (browser provides the answer)
- "Calculate 5000 * 3" — approval tool (asks you before running)
- "Remind me in 5 minutes to take a break" — scheduling
src/
server.ts # Chat agent with tools and scheduling
app.tsx # Chat UI built with Kumo components
client.tsx # React entry point
styles.css # Tailwind + Kumo styles
- AI Chat — Streaming responses powered by Workers AI via
AIChatAgent - Three tool patterns — server-side auto-execute, client-side (browser), and human-in-the-loop approval
- Scheduling — one-time, delayed, and recurring (cron) tasks
- Reasoning display — shows model thinking as it streams, collapses when done
- Debug mode — toggle in the header to inspect raw message JSON for each message
- Kumo UI — Cloudflare's design system with dark/light mode
- Real-time — WebSocket connection with automatic reconnection and message persistence
Update the name in package.json and wrangler.jsonc — the name in wrangler.jsonc becomes your deployed Worker's URL (<name>.<subdomain>.workers.dev).
Edit the system string in server.ts to give your agent a different personality or focus area. This is the most impactful single change you can make.
The starter ships with demo tools (getWeather returns random data, calculate does basic arithmetic). Replace them with real implementations:
// In server.ts, replace a demo tool with a real API call:
getWeather: tool({
description: "Get the current weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => {
const res = await fetch(`https://api.weather.example/${city}`);
return res.json();
}
}),Add new tools to the tools object in server.ts. There are three patterns:
// Auto-execute: runs on the server, no user interaction
myTool: tool({
description: "...",
inputSchema: z.object({ /* ... */ }),
execute: async (input) => { /* return result */ }
}),
// Client-side: no execute function, browser provides the result
// Handle it in app.tsx via the onToolCall callback
browserTool: tool({
description: "...",
inputSchema: z.object({ /* ... */ })
}),
// Approval: add needsApproval to gate execution
sensitiveTool: tool({
description: "...",
inputSchema: z.object({ /* ... */ }),
needsApproval: async (input) => true, // or conditional logic
execute: async (input) => { /* runs after approval */ }
}),When a scheduled task fires, executeTask runs on the server. It does its work and then uses this.broadcast() to notify connected clients (shown as a toast notification in the UI). Replace it with your own logic:
async executeTask(description: string, task: Schedule<string>) {
// Do the actual work
await sendEmail({ to: "user@example.com", subject: description });
// Notify connected clients
this.broadcast(
JSON.stringify({ type: "scheduled-task", description, timestamp: new Date().toISOString() })
);
}Why
broadcast()instead ofsaveMessages()? Injecting into chat history can cause the AI to see the notification as new context and re-trigger the same task in a loop.broadcast()sends a one-off event that the client displays separately from the conversation.
If you don't need scheduling, remove scheduleTask, getScheduledTasks, and cancelScheduledTask from the tools object, the executeTask method, and the schedule-related imports (getSchedulePrompt, scheduleSchema, Schedule, generateId).
Use this.setState() and this.state for real-time state that syncs to all connected clients. See Store and sync state.
Expose agent methods as typed RPC that your client can call directly:
import { callable } from "agents";
export class ChatAgent extends AIChatAgent<Env> {
@callable()
async getStats() {
return { messageCount: this.messages.length };
}
}
// Client-side:
const stats = await agent.call("getStats");See Callable methods.
Add external tools from MCP servers:
async onChatMessage(onFinish, options) {
// Connect to an MCP server
await this.mcp.connect("https://my-mcp-server.example/sse");
const result = streamText({
// ...
tools: {
...myTools,
...this.mcp.getAITools() // Include MCP tools
}
});
}See MCP Client API.
The starter uses Workers AI by default (no API key needed). To use a different provider:
npm install @ai-sdk/openai// In server.ts, replace the model:
import { openai } from "@ai-sdk/openai";
// Inside onChatMessage:
const result = streamText({
model: openai("gpt-5.2")
// ...
});Create a .env file with your API key:
OPENAI_API_KEY=your-key-here
npm install @ai-sdk/anthropicimport { anthropic } from "@ai-sdk/anthropic";
const result = streamText({
model: anthropic("claude-sonnet-4-20250514")
// ...
});Create a .env file with your API key:
ANTHROPIC_API_KEY=your-key-here
npm run deployYour agent is live on Cloudflare's global network. Messages persist in SQLite, streams resume on disconnect, and the agent hibernates when idle.
MIT