Cloudflare Email Service vs an Agent Inbox
Cloudflare Email Service is the most credible DIY base for agent email: cheap at volume, your own domain today, code at the edge. It hands you transport and routing. Parsing, storage, threading, safe retries, per-agent grants, approval and audit are yours to build. We ship those; we lose on price and domains.
At a glance
| Dimension | Email for Agents | Cloudflare Email Service |
|---|---|---|
| Status | Live on agents.emailforagents.ai. Paid plans are proposed and not purchasable | Email Routing is available on Workers Free and Paid. Email Sending has been in public beta since 16 April 2026, on Workers Paid |
| Free tier For inbound volume on a free plan, Cloudflare wins outright. | One live inbox and 100 email units a month per verified owner identity | Workers Free: inbound routing with no charge or volume cap; sending to arbitrary recipients not available. Sends to verified destination addresses are free on all plans |
| Paid sending They win on price at any volume. | No plan can be bought yet | Workers Paid ($5 a month account minimum): 3,000 emails included, then $0.35 per 1,000 |
| Your own domain They win this row. | Requires a paid plan, and paid plans are not purchasable yet | Required to send to arbitrary recipients, and the domain must use Cloudflare DNS. Onboarding adds the SPF, DKIM and DMARC records |
| Inbound storage | Stored threads and messages per inbox, readable over REST and MCP | A Worker receives a raw MIME stream per message. Storage is whatever you write to R2, D1 or a Durable Object |
| Retry safety | Idempotency-Key on sends; a reused key with different content fails with idempotency_conflict | No idempotency key is documented on the Workers binding or the REST send endpoint |
| Per-agent permissions Their binding allowlist is a platform-enforced recipient limit we do not mirror one-for-one. | 32 named scopes, credentials pinned to inbox ids, MCP tool list filtered by grant | Per-binding sender and recipient allowlists; account API tokens with permission to send emails |
| Human approval | An approval resource, pending for up to seven days, emitting approval.requested and approval.rejected | No approval primitive in the Email Service docs. The open-source Agentic Inbox app requires explicit confirmation before sending |
| Test mode | Non-routable .mail.invalid inboxes with a simulated transport and an efa_test_ key prefix | wrangler dev simulates the send binding and logs to the console, and accepts simulated inbound mail at a local endpoint; remote bindings send real mail |
Choose Cloudflare Email Service if
- You already run on Workers and want the agent's mail to be one more binding next to its Durable Object, R2 bucket and AI model.
- You need your own domain on the agent's address today. Cloudflare gives it to you on Workers Paid; our custom domains sit behind a plan that cannot be bought yet.
- Volume matters. Workers Paid includes 3,000 outbound emails a month, then $0.35 per 1,000, and inbound routing carries no charge or volume cap on either plan.
- You want every line of the mailbox logic in your own repository, including the model that reads mail, the storage schema and the UI.
- A hard recipient allowlist at the binding, enforced by the platform, is the whole control you need for a narrow notification agent.
Choose Email for Agents if
- You want a stored, threaded mailbox per agent over REST and remote MCP without writing a MIME parser, a thread table or a reply resolver.
- A retried send must not become a second email: an Idempotency-Key is honoured, and reusing one with different content is refused.
- Several agents share a project and each credential has to be limited to named scopes and specific inboxes, not one account token.
- Outbound mail needs a human decision recorded as an approval resource with an expiry and events, not a confirm button inside one app.
- You want to rehearse the inbound loop against a test inbox that cannot reach an internet recipient, with a key prefix that cannot touch live.
What Cloudflare actually ships for agent email today
Cloudflare Email Service vs an agent inbox is a build-versus-buy question, and Cloudflare is the most credible “build” answer on the market. Its 16 April 2026 post, titled “Email for agents”, moved Email Sending from private to public beta and folded it together with Email Routing into one product. The docs overview today lists Email Sending as beta on the Workers Paid plan and Email Routing on Free and Paid.
There are four pieces, and it helps to name them precisely because comparison pages keep blurring them:
- Email Routing delivers inbound mail to a Worker’s
email()handler as aForwardableEmailMessage: envelopefromandto, aHeadersobject, arawReadableStreamof MIME, andforward(),reply()andsetReject(). - Email Sending is a
send_emailbinding (env.EMAIL.send()), a REST endpoint at/accounts/{account_id}/email/sending/send, and authenticated SMTP. - The Agents SDK adds
onEmail(),sendEmail(),replyToEmail()androuteAgentEmail()with resolvers that pick which agent instance gets a message. - Agentic Inbox is an Apache 2.0 reference app on GitHub: a web mail client with an AI agent, built on those pieces plus Durable Objects, R2 and Workers AI.
The first three are platform. The fourth is a sample you deploy and own. That line matters for everything below. If you want the category overview first, start with what email for AI agents means in practice.
The build ledger: what each control costs on Cloudflare
This is the part the other comparisons skip. An agent that reads mail from strangers and can reply needs a fixed set of controls, whoever hosts it. The table maps each one to the Cloudflare primitive that exists today and to what you still write.
| Control | Cloudflare primitive | What you still build |
|---|---|---|
| MIME parsing | message.raw stream; docs recommend postal-mime | Attachment extraction, charset edge cases, size handling |
| Storage | None in Routing; R2, D1, Durable Object SQLite available | Message schema, retention, deletion, attachment lifecycle |
| Threading | message.reply() keeps the Message-ID chain, once | A thread model; References and In-Reply-To on every later send |
| Reply routing | createSecureReplyEmailResolver with HMAC-signed headers | Secret rotation, expiry policy, fallback when a client strips headers |
| Idempotent sends | None documented | A send ledger keyed by your own id, plus a decision on timeouts |
| Per-agent permissions | Binding allowlists; account API tokens | Per-agent read, draft and send separation; per-inbox scoping |
| Human approval | None in the platform | Pending state, reviewer UI, expiry, re-check of content at approval time |
| Audit trail | Sending metrics and logs | Who authorised which send, from which credential, with which payload |
| Test mode | Send and inbound simulation in wrangler dev | Stored fixtures, a CI path that exercises read, reply and thread end to end |
None of those rows is exotic engineering. Each one is a week you did not plan for, and a few of them are the rows that cause incidents. The sections below go through the ones where the Cloudflare docs contain a detail that changes the design.
Inbound: a stream, not a mailbox
Email Routing gives your Worker one invocation per message and a raw MIME stream.
There is no stored mailbox behind it. If your handler throws, runs out of CPU, or
you did not persist the message before returning, the message is not somewhere you
can list later. The limits page is explicit that on Workers Free, complex handlers
can exceed CPU and memory limits and fail with EXCEEDED_CPU, and it recommends Workers
Paid for that reason. So the realistic inbound design is: parse with postal-mime,
write the parsed record and attachments to storage inside the handler, then hand off.
Agentic Inbox does exactly this, one Durable Object with SQLite per mailbox and R2 for
attachments, which is a good design and also code you now maintain.
Replying inside the handler has sharp rules, and they are worth knowing before you
build around reply(). The email handler docs state that reply() throws unless the
incoming email has a valid DMARC result, the reply goes to the original sender, the
outgoing sender domain matches the domain that received the mail, and it is the only
reply for that event.
It also throws if the incoming message carries more than 100 References entries,
a deliberate loop brake. That means the one-shot synchronous reply is unavailable for
any sender with a failing or missing DMARC result, and any asynchronous reply, the kind
an agent sends an hour later after checking three systems, goes out through
send_email instead. On that path, threading is your job. The Agents SDK’s
sendEmail() takes an inReplyTo option that sets In-Reply-To, but keeping the
Message-ID you are answering, and building References, comes from your own storage. The failure mode is quiet, because
a broken References chain forks the conversation in the recipient’s client while
your send succeeds. Threading for agents covers
why this breaks in practice.
The Agents SDK closes part of this gap. sendEmail() injects X-Agent-Name and
X-Agent-ID headers and, given a secret, signs them with HMAC-SHA256 and a
timestamp; createSecureReplyEmailResolver verifies the signature and expiry before
routing the reply back to the same agent instance. That is a clean answer to “which
agent owns this reply”. It is not a stored thread, and it depends on the reply
carrying the signed headers back.
Here the inbox is the stored object. Threads and messages are REST resources under
/projects/{project_id}/inboxes/{inbox_id}/threads, and the same data sits behind the
list_threads, list_messages and get_message MCP tools. Nothing needs to be caught
in a handler to exist later.
Outbound: retries, and the missing idempotency key
This is the most concrete gap for an agent workload. Neither the Workers API page nor
the REST API page documents an idempotency key. The binding returns a messageId on
success and throws an Error with a code on failure, including
E_RATE_LIMIT_EXCEEDED, E_DAILY_LIMIT_EXCEEDED, E_DELIVERY_FAILED and
E_INTERNAL_SERVER_ERROR. The REST response sorts recipients into delivered,
permanent_bounces and queued.
Now put an agent in front of it. A tool call times out after the request left your
Worker. The model, reasonably, calls the tool again. Without a key the platform can
dedupe on, the only protection is a ledger you write: record an intent keyed by your
own id before calling send(), and decide what “timed out, outcome unknown” means
before retrying. Skip that and the recipient gets the email twice. The full failure
shape is in why an agent sends the same email twice.
On this side a send accepts an Idempotency-Key. The key and a SHA-256 of the payload
are stored together, so a retry with the same content inside the 30-day replay window returns the
original result, and a retry with different content under the same key fails with idempotency_conflict
instead of sending something new (packages/domain/src/services.ts, around line 1616).
The harder case stays honest: if the provider’s answer is lost, the message is recorded
as message.submission_unknown and it does not resolve itself later in this build.
Cloudflare is better on sending economics, and it is not close. Workers Paid includes 3,000 outbound emails a month per account, then $0.35 per 1,000. Emails rejected at the API boundary, including suppressed recipients, do not count. Inbound carries no charge or volume cap on either plan. New accounts start on a conservative daily quota that rises with sending reputation, recipients are capped at 50 per message, and total message size is 5 MiB, or 25 MiB to verified destination addresses.
Permissions: account tokens and binding allowlists
Cloudflare’s sending controls live in two places. The REST endpoint authenticates with
an account API token that has permission to send emails, and the sender domain must be
onboarded on the account that owns the token. A send_email binding
can be restricted with destination_address, allowed_destination_addresses or
allowed_sender_addresses; a send outside the list fails with
E_RECIPIENT_NOT_ALLOWED. That allowlist is a real, platform-enforced control, and
for a notification agent that only ever emails ops, it is the whole security model you
need. Credit where due: we do not ship a one-for-one equivalent of a fixed recipient
allowlist on the binding.
What the platform does not describe is separating reading from drafting from sending
for a given agent, because reading is your code touching your storage. Agentic Inbox
shows where that leaves a real app. Its README says any user who passes the shared
Cloudflare Access policy can access all mailboxes, including through the MCP server
at /mcp, where an external tool “can operate on any mailbox by passing a mailboxId
parameter”, and that “there is no per-mailbox authorization; the Cloudflare Access
policy is the single trust boundary.” That is a sensible choice for a single-owner
inbox and a stated one. It is also the boundary you must redesign the moment two agents
with different jobs share a deployment.
Here that boundary is the product. There are 32 named scopes, messages:read and
drafts:write separable from messages:send, and credentials pinned to specific
inbox ids. The MCP server filters tools/list by the connection grant and re-checks
on tools/call, so a read-and-draft grant yields a session where send_message does
not exist. An API key is rejected at https://mcp.emailforagents.ai/mcp by design;
only an OAuth connection grant may call tools. The model is laid out on the
developers page, and the reasoning in
agent email permissions and scopes.
Approval and audit: a confirm button versus a record
Agentic Inbox’s README says its agent auto-drafts replies to inbound mail, “always requiring explicit confirmation before sending”. That is the right instinct and it is a UI behaviour of one app. The Email Service docs describe no pending-approval state, no expiry and no event when a reviewer says no, so a second agent written against the raw binding gets none of it.
Building a real approval step on Cloudflare means a pending record, a reviewer screen,
an expiry, and one easy-to-miss rule: the approved content must be the content that
leaves. If the agent edits a draft after a human read it, the approval should not carry
over. Here a held send writes an approval in state pending that expires after seven
days (services.ts:1662), emits approval.requested, and emits approval.rejected
when a reviewer refuses. Approving a draft whose content no longer matches the content
the approval was raised for is refused with version_conflict (services.ts:2051). Two further approval events are declared in the contract but not
emitted in this build, so do not build on them. The runtime side is on the
controls page, and the design is in
human approval for agent email.
Audit follows the same pattern. Cloudflare gives you sending metrics and logs, and notes that sends from a Worker show as “dropped” in the Email Routing summary even when delivered, so outbound tracking belongs in the Email Sending observability view. Who authorised a given send, and with which credential, is a table you design. Here the console shows every thread and human control changes are written to an audit log with the actor and a before and after diff. Audit logs are excluded from project exports, which is a gap if you need them in your own warehouse.
Testing without sending
Cloudflare’s local story is good, and it covers both directions. By default
wrangler dev simulates the send binding and logs sends to the console, remote bindings
let a local Worker send real mail through the service, and a raw RFC 5322 message
POSTed to /cdn-cgi/local/email invokes your local email() handler as if it had
arrived. What that exercises is the handler. Whether the agent can later list the
message, reply to it and keep the thread intact depends on the storage and threading
code you wrote, so a CI test of the full loop is a harness you build around it.
Here test inboxes use a non-routable .mail.invalid address and a simulated transport,
up to three active per workspace, the last 100 messages retained, content expiring
after seven days, and
/projects/{project_id}/test/inbound injects mail into the agent’s own inbox. An
efa_test_ key resolves only to test projects, and no flag lets it reach live. The
walkthrough is in testing agent email without sending.
Cloudflare Email Service vs agent inbox: which to pick
The recommendation flips on one question: is the agent’s email a feature of a Workers app you already own, or a separate identity several agents and humans need to share safely?
If it is a feature, build on Cloudflare. You get your own domain today with SPF, DKIM and DMARC set up on onboarding, sending that costs a fraction of a cent per message, free inbound with no volume cap, and the agent’s state, storage and model on one platform. The constraints are real but acceptable: the domain must use Cloudflare DNS, Email Sending is still beta, and every row of the build ledger above lands in your backlog. If a binding-level recipient allowlist covers your risk, you may never need the rest.
If it is an identity, the ledger is the product you would be building. That is what we ship, at the cost of quota, price and custom domains, all of which Cloudflare wins today. The comparisons hub lines this up against the other options.
Sources
Everything stated above about Cloudflare Email Service was read from these pages on . Products change; if a claim here is stale, it is a bug worth reporting.
- https://blog.cloudflare.com/email-for-agents/
- https://developers.cloudflare.com/changelog/post/2026-04-16-email-sending-public-beta/
- https://developers.cloudflare.com/email-service/
- https://developers.cloudflare.com/email-service/platform/pricing/
- https://developers.cloudflare.com/email-service/platform/limits/
- https://developers.cloudflare.com/email-service/get-started/send-emails/
- https://developers.cloudflare.com/email-service/api/send-emails/workers-api/
- https://developers.cloudflare.com/email-service/api/send-emails/rest-api/
- https://developers.cloudflare.com/email-service/configuration/send-bindings/
- https://developers.cloudflare.com/email-service/api/route-emails/email-handler/
- https://developers.cloudflare.com/email-service/local-development/sending/
- https://developers.cloudflare.com/email-service/local-development/routing/
- https://developers.cloudflare.com/agents/communication-channels/email/
- https://developers.cloudflare.com/agents/examples/email-agent/
- https://developers.cloudflare.com/workers/platform/pricing/
- https://github.com/cloudflare/agentic-inbox