Cloudflare Agents
Route signed inbound mail to a durable Agent instance, keyed by inbox or thread.
Cloudflare Agents has a native Email channel
for addresses routed through Cloudflare Email Service. Use that when the mailbox
itself should live entirely inside Cloudflare. Use CarlyEmail when you want the
same onEmail-style model with programmatic inbox creation, CarlyEmail custom
domains, hosted MCP tools, or portability to runtimes outside Cloudflare.
The bridge keeps Cloudflare's two useful properties: an incoming-email lifecycle hook, and a stable identifier that routes later messages back to the same durable agent. CarlyEmail supplies the inboxes and signed delivery event; a Worker supplies the durable execution model.
Webhooks are the right transport for this pattern. They wake the Worker, retry failures, and do not require a Durable Object to stay attached to a socket. Use the WebSocket stream for an active console or long-running agent process instead.
The JavaScript SDK's carlyemail/webhooks export uses Web Crypto only. It runs
unchanged in Workers, verifies the raw body and replay window, and exposes an
onEmail(event) callback.
Install
npm install carlyemail agents
curl -fsS https://raw.githubusercontent.com/shirschfield/carlyemail/main/cli/webhooks.js \
-o src/carlyemail-webhooks.js
curl -fsS https://raw.githubusercontent.com/shirschfield/carlyemail/main/cli/webhooks.d.ts \
-o src/carlyemail-webhooks.d.ts
Note
The Worker helper is complete in the repository but is not in npm's current
carlyemail@0.3.0. These two dependency-free files are the honest install path
until 0.4.0 is published.
Store these as Worker secrets:
npx wrangler secret put CARLYEMAIL_API_KEY
npx wrangler secret put CARLYEMAIL_WEBHOOK_SECRET
One durable agent per inbox
TypeScript
import { CarlyEmail } from "carlyemail";
import {
createEmailHandler,
type CarlyEmailEvent,
} from "./carlyemail-webhooks";
import { Agent, callable, getAgentByName } from "agents";
export class EmailAgent extends Agent<Env> {
@callable()
async onCarlyEmail(event: CarlyEmailEvent) {
const message = event.message!;
const carly = new CarlyEmail({ apiKey: this.env.CARLYEMAIL_API_KEY });
// Put your model or workflow here. This simple version proves the channel.
await carly.messages.reply(
message.inbox_id as string,
message.message_id as string,
{ text: "Thanks — I received this and will follow up." },
);
}
}
export default {
async fetch(request: Request, env: Env) {
const handleEmail = createEmailHandler({
secret: env.CARLYEMAIL_WEBHOOK_SECRET,
async onEmail(event) {
const inboxId = event.message!.inbox_id as string;
const agent = await getAgentByName(env.EmailAgent, inboxId);
await agent.onCarlyEmail(event);
},
});
return handleEmail(request);
},
} satisfies ExportedHandler<Env>;
Register the Worker's URL as a CarlyEmail webhook for message.received. Using
inbox_id as the Agent name gives each inbox isolated state and ensures every
later message returns to the same instance. Use thread_id instead when each
email conversation should have its own durable state.
npx carlyemail webhook https://your-worker.example/hooks/carlyemail \
--events message.received
Delivery and retries
The helper returns 400 for a missing, stale, or invalid signature. It lets an
onEmail exception become a 5xx, so CarlyEmail retries the committed event
instead of acknowledging work that did not finish.
Delivery is at least once. Persist event_id before side effects when duplicate
replies would be harmful. Cloudflare Agents are single-threaded per instance,
which removes concurrent state races; it does not by itself deduplicate a
delivery retry.
Warning
An email body is untrusted input, even when the webhook signature is valid—the signature proves CarlyEmail delivered it, not that the sender's instructions are safe. Scope the CarlyEmail key to one inbox and the minimum permissions the agent needs.
No Worker required
For a Python application, create_email_router gives
FastAPI the same verified callback. For an agent client that already understands
MCP, connect it directly to https://api.carlyemail.com/mcp.