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, task scheduling, and vision (image input).
npx create-cloudflare@latest --template cloudflare/agents-starter
cd agents-starter
npm install
npm run dev
Open http://localhost:5173 to see your agent in action.
Try these prompts to see the different features:
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
AIChatAgentUpdate 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/anthropic
import { 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 deploy
Your agent is live on Cloudflare's global network. Messages persist in SQLite, streams resume on disconnect, and the agent hibernates when idle.
MIT
$ claude mcp add agents-starter \
-- python -m otcore.mcp_server <graph>