Workshop: Build an AI email agent with Cloudflare Agents SDK and Email Routing.
An AI Inbox Agent for FastFlare Home Services — a small business offering cleaning, plumbing, and landscaping. The owner currently manages a chaotic inbox of booking requests, status inquiries, follow-ups, and emergency calls.
By the end of this workshop, they'll have an agent that:
- Receives email via Cloudflare Email Routing
- Understands intent using an LLM
- Maintains conversation history across threads
- Queries a built-in SQL database
Not an auto-responder — a programmable, edge-native team member.
Five concepts, each building on the previous one. This repo covers the first three:
- Email triggers a persistent agent — Email wakes up a persistent entity
- AI = Understanding — That entity reads and interprets your email
- Persistence = Memory — That entity remembers you
- Tools = Action — That entity acts on your behalf (coming soon)
- Scheduling = Proactivity — That entity acts on its own (coming soon)
Email arrives (SMTP)
│
▼
┌─────────────────────┐
│ Email Routing │ Cloudflare Email Routing
└─────────────────────┘
│
▼
┌─────────────────────┐
│ email() handler │ Worker entry point
│ + Resolver │ Routes by sender → agent instance
└─────────────────────┘
│
▼
┌─────────────────────┐
│ SupportAgent │ Durable Object (Agent)
│ ┌───────────────┐ │
│ │ onEmail() │ │ Handles the email
│ │ this.sql │ │ Per-instance SQLite
│ │ this.state │ │ Working context
│ └───────────────┘ │
└─────────────────────┘
Key concepts:
- An Agent is a Durable Object — it hibernates when idle, wakes on demand, and has its own SQLite storage
- The resolver determines which agent instance handles each email — our custom resolver routes by sender so each customer gets their own isolated agent
- One class (
SupportAgent), many instances — one per customer
- Cloudflare account (free tier works) with a registered domain
- Node.js installed
- A code editor
- Terminal / command line
Clone this repo and install dependencies:
git clone https://github.com/harshil1712/email-agent-workshop-code.git
cd email-agent-workshop-code
npm installemail-agent-workshop/
├── src/
│ └── index.ts ← All our code goes here
├── wrangler.jsonc ← Worker + agent configuration
├── package.json
└── tsconfig.json
- -> Chapter 1 - Email Triggers a Persistent Agent (this branch)
- Chapter 2 - AI = Understanding
- Chapter 3 - Persistence = Memory
Email doesn't trigger a function — it wakes up a persistent entity.
- An Agent is a Durable Object that hibernates when idle and wakes on demand
- The resolver pattern determines which agent instance handles each email
- Custom sender-based routing gives each customer their own agent instance
Add the send_email binding to wrangler.jsonc:
- Import
postal-mimeand theAgentEmailinterface, androuteAgentEmailfromagents:
import { Agent, routeAgentEmail, routeAgentRequest } from 'agents';
import { type AgentEmail } from 'agents/email';
import PostalMime from 'postal-mime';- Add an
onEmailhandler to theSupportAgentclass:
async onEmail(email: AgentEmail) {
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
console.log('Subject:', parsed.subject);
console.log('From:', parsed.from);
console.log('Text body:', parsed.text);
}- Add an
emailhandler to the default export with a custom resolver that routes by sender address — so each customer gets their own agent instance:
export default {
async email(message, env) {
await routeAgentEmail(message, env, {
resolver: async (email) => ({
agentName: 'SupportAgent',
agentId: email.from,
}),
});
},
async fetch(request: Request, env: Env) {
return (await routeAgentRequest(request, env)) || new Response('Not found', { status: 404 });
},
} satisfies ExportedHandler<Env>;- Start the development server:
npm run dev- Send a test email via the local email endpoint:
curl --request POST 'http://localhost:8787/cdn-cgi/handler/email' \
--url-query 'from=alice@abc.com' \
--url-query 'to=support@example.com' \
--data-raw 'From: "Alice" <alice@abc.com>
To: support@example.com
Subject: Help with my order
Content-Type: text/plain; charset="utf-8"
Message-ID: <test-email-001@localhost>
Hi there, I need help with my recent order #12345.'Expected console output:
Subject: Help with my order
From: { address: 'alice@abc.com', name: 'Alice' }
Text body: Hi there, I need help with my recent order #12345.
- The resolver decides which agent instance wakes up
- A custom resolver routes by sender —
alice@abc.comalways hits instancealice@abc.com - A single inbound address can serve many isolated per-customer agents