← posts / ai integrations

Self-host an AI email agent on Cloudflare Workers: agentic-inbox set up and costed

Cloudflare's open-source agentic-inbox runs a full email client on Workers, with one SQLite Durable Object per mailbox and a Kimi K2.5 agent that drafts replies. Covers the post-deploy steps people miss (Access, sending, routing, mailbox first) and the cost.

if.codesOct 1, 2026 · 8 min read#cloudflare#workers#ai-agents#email#self-hostingAI-assisted

Cloudflare open-sourced agentic-inbox, a complete email client that runs on your own Cloudflare account. Inbound mail arrives through Email Routing, each mailbox is a Durable Object with its own SQLite database, attachments go to R2, and an Agents SDK agent reads new mail and writes a draft reply. It never sends on its own; you review the draft and send it yourself.

The Deploy button handles only part of the setup. The README warns that you must also complete the "After deploying" steps, and most failed installs skip them. This guide walks through all of the steps, then works out the cost from Cloudflare's published prices.

What you are deploying

Everything below comes from the repo's wrangler.jsonc and workers/ source (commit 48039bb, April 2026):

A conceptual visualization of an AI-powered email system.
A conceptual visualization of an AI-powered email system.
  • One Worker (workers/app.ts), built with Hono and React Router. It serves the UI and API, and its email() handler receives mail from Email Routing.
  • Three SQLite-backed Durable Object classes: MailboxDO (one per mailbox, stores the emails), EmailAgent (one per mailbox, an AIChatAgent with 9 email tools) and EmailMCP (an MCP server at /mcp).
  • Bindings: BUCKET (R2 bucket agentic-inbox), AI (Workers AI) and EMAIL (a send_email binding for outbound mail).
  • Models: the agent uses @cf/moonshotai/kimi-k2.5. Before any draft is written, a prompt-injection check runs on @cf/meta/llama-3.1-8b-instruct-fast. When the model returns the draft as plain text instead of calling the draft tool, @cf/meta/llama-4-scout-17b-16e-instruct cleans that text up.

Prerequisites: a Cloudflare account with a domain on Cloudflare DNS. Email Routing does not work without it. You also need Node.js and npm for the deploy, a Cloudflare Zero Trust organization (created the first time you open Zero Trust in the dashboard) for Access, and, for the latency check at the end, the cloudflared CLI.

Step 1: Deploy

The simplest route is the Deploy to Cloudflare button in the README. It creates R2, Durable Objects and Workers AI for you and asks for DOMAINS, the domain that will receive your mail (for example example.com). To deploy from the CLI instead:

git clone https://github.com/cloudflare/agentic-inbox
cd agentic-inbox
npm install
npx wrangler r2 bucket create agentic-inbox
# edit wrangler.jsonc: "vars": { "DOMAINS": "yourdomain.com", ... }
npm run deploy

npm run deploy runs npm run build && wrangler deploy, where build is react-router build. You can also set EMAIL_ADDRESSES in vars. When it holds values, the Worker accepts mail only for those addresses and creates mailboxes only for them.

Step 2: Turn on Cloudflare Access (required)

This step is required, not optional hardening. Outside local dev, the Worker's middleware fails closed. When POLICY_AUD or TEAM_DOMAIN is missing, every request gets a 500 with Cloudflare Access must be configured in production. When a request has no valid Access JWT, it gets a 403.

An illustration representing secure access and authentication layers.
An illustration representing secure access and authentication layers.
  1. In the dashboard, open the Worker and go to Settings > Domains & Routes. Enable one-click Cloudflare Access.
  2. The modal shows two values, POLICY_AUD and TEAM_DOMAIN. Store both as secrets:
npx wrangler secret put POLICY_AUD
npx wrangler secret put TEAM_DOMAIN

TEAM_DOMAIN accepts either the team URL (https://your-team.cloudflareaccess.com) or the full .../cdn-cgi/access/certs URL. If you later see Invalid or expired Access token, the README's fix is to turn Access off and on again, then re-set both secrets from the new modal.

The trust model: anyone who passes your Access policy can read every mailbox, including through the /mcp endpoint by passing a mailboxId. The app has no per-mailbox authorization. Write the Access policy with that in mind.

Step 3: Enable Email Service for sending

The Worker sends outbound mail through the EMAIL send_email binding. In the maintainers' step-by-step guide, enabling email sending for your domain is a separate step. Do it for the same domain you entered as DOMAINS. According to the Email Service pricing page, sending to arbitrary recipients requires the Workers Paid plan.

Step 4: Route inbound mail to the Worker

  1. Go to Compute > Email Service > Email Routing, choose Onboard Domain, pick your domain and accept the DNS records. Cloudflare adds MX, SPF and DKIM records.
  2. Open Routing Rules, enable the Catch-all rule, set the action to Send to a Worker, choose agentic-inbox and save. If you want only some addresses handled by the app, create per-address rules instead of the catch-all.

Watch the catch-all. Every message the Worker receives for an existing mailbox triggers an auto-draft (receiveEmail() calls the agent's onNewEmail through ctx.waitUntil), and each draft is a Workers AI call. A catch-all on a domain that gets a lot of spam turns that spam into AI spend. Prefer per-address rules for the mailboxes you actually use.

Want AI wired into the systems you already run?I build LLM integrations with costs and quality you can see. The estimate is free.

Step 5: Create the mailbox before you test

This is the step people miss most often. receiveEmail() looks for mailboxes/<address>.json in R2. If that object is missing, the message is dropped and the only trace is this log line:

Ignoring email for hello@yourdomain.com: mailbox does not exist

The sender gets no bounce, because the handler returns normally. So open the deployed app, create a mailbox (for example hello@yourdomain.com), and only then send a test message to it.

Step 6: Watch it work

npx wrangler tail agentic-inbox --format pretty

Send a test message from an outside address. The tail shows the email invocation, followed by the EmailAgent call to /onNewEmail. If the injection scanner flags the message, or the scanner itself fails, you will see Skipping auto-draft due to detected prompt injection. The code fails closed here: the email is still saved, but no draft is written.

Measure draft latency yourself

Both timestamps are server-side. The inbox copy's date is set to new Date() when the message is received (the source comments that it "uses receive time, not the email's Date header"). The draft's date is set to new Date() when draft_reply saves it. Subtract the first from the second to get the time from arrival to saved draft. The API sits behind Access, so call it with cloudflared:

BASE=https://agentic-inbox.<your-subdomain>.workers.dev
MB=hello@yourdomain.com
cloudflared access login "$BASE"
cloudflared access curl "$BASE/api/v1/mailboxes/$MB/emails?folder=inbox&limit=5"
cloudflared access curl "$BASE/api/v1/mailboxes/$MB/emails?folder=draft&limit=5"

Match the two messages on thread_id and compare their date fields. We have not deployed this ourselves, so we have no number to report. Run it on your own traffic.

What it costs

All prices below are copied from Cloudflare's pricing pages as of this writing. Check the linked pages before you budget.

A representation of calculating operational costs and resource usage.
A representation of calculating operational costs and resource usage.
  • Workers Paid: "$5 USD per month" per account. Includes 10 million requests and 30 million CPU ms per month, then $0.30 per additional million requests and $0.02 per additional million CPU ms. (Workers pricing)
  • Email Service: receiving through Email Routing is "Unlimited" on both plans. Sending is "Not available" on Workers Free. On Workers Paid, 3,000 emails per month are included, then $0.35 per 1,000. Sends to verified destination addresses in your own account are free. (Email Service pricing)
  • Durable Objects (Paid): 1 million requests per month included, then $0.15 per million. 400,000 GB-s included, then $12.50 per million GB-s. SQLite storage: first 25 billion row reads per month included (then $0.001 per million), first 50 million row writes included (then $1.00 per million), and 5 GB-month of stored data (then $0.20 per GB-month). Incoming WebSocket messages, which is how the agent panel communicates, are billed at a 20:1 ratio. (Durable Objects pricing)
  • R2 Standard: $0.015 per GB-month, with 10 GB-month free. Class A operations cost $4.50 per million and Class B $0.36 per million, with 1M and 10M free respectively. Egress is free. (R2 pricing)
  • Workers AI: 10,000 Neurons per day free (one account-wide pool shared by every model, so the Kimi drafts, the Llama 3.1 scanner and the Llama 4 Scout cleanup all draw from it), then $0.011 per 1,000 Neurons. kimi-k2.5 costs $0.600 per M input tokens, $0.100 per M cached input tokens and $3.000 per M output tokens (54,545 / 9,091 / 272,727 neurons per M). llama-4-scout-17b-16e-instruct costs $0.270 per M input and $0.850 per M output. The pricing table does not list the exact llama-3.1-8b-instruct-fast ID the scanner calls. The closest entry, llama-3.1-8b-instruct-fp8-fast, is $0.045 per M input and $0.384 per M output. (Workers AI pricing)

Per-draft AI cost: the formula

Kimi does the drafting, so it dominates the cost. The auto-draft call can run for up to 5 steps (stopWhen: stepCountIs(5)), and each tool round trip sends the context again. That makes a fixed per-email token count impossible to predict. Use this formula instead:

cost ≈ input_tokens × $0.60/M + output_tokens × $3.00/M   (kimi-k2.5, uncached)

A hypothetical draft that uses 5,000 input tokens and 500 output tokens comes to $0.003 + $0.0015 = $0.0045. That is about 409 neurons, so the 10,000-neuron daily free allocation would cover at most roughly 24 drafts of that size before you start paying. The injection scan on every incoming message (and any Llama 4 Scout cleanup) comes out of the same pool, so the real number is somewhat lower. These token counts are an illustration, not a measurement. To get your real numbers, check Workers AI usage in the dashboard after a day of real mail.

Bottom line: because sending requires Workers Paid, the realistic minimum is $5 per month. A personal inbox is likely to stay inside the included Durable Object, R2 and email allowances. Beyond that, you pay per token for Kimi drafts once the daily free Neurons run out.

Checklist

  1. Domain on Cloudflare DNS; deploy with DOMAINS set.
  2. One-click Access enabled; POLICY_AUD and TEAM_DOMAIN set as secrets.
  3. Email sending enabled for the domain (requires Workers Paid for arbitrary recipients).
  4. Email Routing onboarded; catch-all or per-address rule set to Send to a Worker.
  5. Mailbox created in the UI before the first test email.
  6. wrangler tail open while you send the test.

Sources

if.codesI build RAG, AI integrations and agent pipelines on Go and Python backends — and write about it here.
// keep reading

More posts on AI and backends.

// free quote

Read something you need? I’ll quote it for free.

RAG, AI integrations, agents or the backend underneath — tell me what you have and what should change. I read every request myself.

Free · no commitment

Tell me what you have. I’ll tell you what it takes.

1Describe the projectA few sentences is enough — about two minutes.
2I review itI read it myself and may ask a follow-up question.
3You get a free quoteScope, approach and estimate — yours to keep, no strings.
What kind of project is it?
Free and without obligation. Your details are used only to reply — see the privacy policy.