Email is the channel agents are best suited to and the one most of them never get.
Best suited, because email is where the hard messages live. Nobody sends a purchase order over WhatsApp. Quotation requests, invoices, supplier confirmations and support threads with six replies and a PDF attached all arrive by email, and every one of them is a task with enough context in it for an agent to actually do the work.
Never get, because giving an agent an inbox means solving deliverability, and deliverability is a discipline with its own decade of accumulated pain.
This covers what it takes.
What an agent with an inbox actually needs
Five things, and only one of them is the model:
| Requirement | Why it is not optional |
|---|---|
| A domain you send from, authenticated | Unauthenticated mail goes to spam or is rejected outright |
| Inbound routing | Somewhere for replies to land as events, not in a mailbox nobody reads |
| Threading | An agent that answers a five-message thread without reading it answers the wrong question |
| Attachments both ways | The PDF is usually where the actual request is |
| A suppression list | Sending to an address that hard bounced burns the reputation you spent months building |
Miss the first and nothing arrives. Miss the last and everything stops arriving, three weeks later, all at once.
Setting up the domain
Email authentication is three records with three jobs. DKIM signs your mail so receivers can verify it came from you. SPF says which servers may send as your domain. DMARC tells receivers what to do when the first two disagree.
terminalnpx zavudev email-domains add example.com
That returns the records to publish. DKIM is required to send at all. SPF, DMARC and a custom MAIL FROM are marked recommended, which understates it: they are what separates the inbox from the promotions tab.
Publish them at your DNS provider, then:
terminalnpx zavudev email-domains verify example.com
Two practical notes. Verification checks what the provider has cached, so correct records can still read as pending for a while; re-running verify does not force a re-scan. And use a subdomain you can afford to burn, like agent.example.com, rather than your main corporate domain. If the agent gets something badly wrong at volume, the damage is contained to a domain you can retire.
Receiving mail
Sending is the easy half. An agent needs replies, which means an MX record and receiving enabled on the sender.
The setting that matters most for agents is catch-all. With it on, the sender receives mail addressed to any local part at the domain, not just its own address. That means orders@, quotes@, support@ and ticket-4471@ all reach the same agent, and the original recipient arrives in the webhook's data.to so the agent knows which one was used.
This is what makes per-thread addressing possible. Give every conversation its own reply address, and threading becomes exact rather than a guess based on subject lines:
quote-8842@agent.example.com -> the quotation thread for deal 8842
ticket-4471@agent.example.com -> support ticket 4471No parsing of Re: Re: Fwd:, no matching on subject, no confusion when two customers open threads about the same product on the same day.
The agent
Inbound email arrives as a message.inbound event like every other channel, so an agent that already answers on WhatsApp answers on email with no new code.
When you want email-specific behaviour, describe it rather than writing it. With the skills installed, the prompt is:
> Add email handling to this agent. Reply addresses look like quote-{dealId}@agent.example.com, so pull the deal ID out of the recipient. Download any attachments and pass them to answerQuotation. Reply on the same thread with the generated PDF attached.
What it writes:
TypeScriptimport { defineFunction } from "@zavudev/functions" import Zavudev from "@zavudev/sdk" const zavu = new Zavudev({ apiKey: process.env.ZAVU_API_KEY }) export default defineFunction({ on: ["message.inbound"], handler: async (event, ctx) => { if (event.data.channel !== "email") return const dealId = event.data.to.match(/quote-(\d+)@/)?.[1] // Attachment listing is REST only: Stainless has not generated it into the // SDK yet, so call the endpoint directly rather than a method that does // not exist. const res = await fetch(, { headers: { Authorization:https://api.zavu.dev/v1/messages/${event.data.messageId}/attachmentsBearer${process.env.ZAVU_API_KEY} } }, ) const { items } = await res.json() const reply = await answerQuotation({ dealId, body: event.data.text, files: items.map((a: { downloadUrl: string }) => a.downloadUrl), }) await zavu.messages.send({ to: event.data.from, channel: "email", subject:Re: quotation${dealId}, text: reply.text, htmlBody: reply.html, attachments: [{ filename: "quote.pdf", content: reply.pdfBase64 }], }) }, })
Two details in there are the whole reason email is different from every other channel.
Attachments arrive as stored files, not as bytes in the webhook. GET /v1/messages/{messageId}/attachments returns a short-lived signed downloadUrl per file, generated fresh on each request. Fetch promptly, do not cache the URL. This is where the actual request usually lives: the spec, the PO, the photo of the damaged item. Note that this endpoint is REST only today. The TypeScript SDK covers sending attachments but not listing them, so call it with fetch as above.
Sending attachments takes base64 or a URL. Either content with base64 bytes or path with a URL the server fetches, up to 40MB total. And content_id lets you embed an image inline, referenced from the HTML body as cid:your_content_id, which is how an agent sends a chart rather than describing one.
The failures that only happen on email
The reply-all loop. Your agent replies to an address that is itself an autoresponder. It replies. Your agent replies. By morning you have four thousand messages and a domain reputation problem. Never reply to an address that has already been answered in the last few minutes, and never to noreply@ or mailer-daemon@.
Answering the wrong question. Email threads carry history, and the newest message is often "sounds good, go ahead" with the actual request four replies up. Feed the agent the thread, not the last message. Per-thread addressing above is what makes retrieving the right thread reliable.
Slow burn deliverability. Nothing fails visibly. Open rates just decline, then replies stop, and by the time anyone investigates the domain has been in the spam folder for a month. The controls that matter: send only to addresses that asked, honour unsubscribes immediately, and validate lists before large sends.
terminalcurl -X POST https://api.zavu.dev/v1/introspect/email \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -d '{"emails": ["maria@example.com", "info@deaddomain.example"]}'
That returns deliverable, risky or undeliverable per address, with the reason: invalid syntax, a domain with no MX records, a disposable inbox, a role address, or an address already on your suppression list from a previous bounce. Drop the undeliverable ones before sending, and a bounce rate that would have taken your domain down stays flat.
On individual sends, addresses that would be a guaranteed hard bounce are failed before dispatch rather than sent, with errorCode set to EMAIL_INVALID_RECIPIENT, EMAIL_DOMAIN_NOT_FOUND or EMAIL_RECIPIENT_SUPPRESSED. Advisory signals like role addresses do not block the send, which is why the batch check above is worth running yourself before anything large.
The agent that answers everything. Email arrives from cold outreach, newsletters, invoices and actual customers. An agent that treats all of it as a task to complete will happily negotiate with a spam bot. Classify first, act second.
What it costs
Email on Zavu is billed from the prepaid balance in blocks of 1,000: $0.40 per 1,000 transactional emails and $0.80 per 1,000 marketing or broadcast emails. A block is charged when your monthly count crosses each 1,000 boundary. Free teams start with $2 of credit and are capped at 3,000 emails per month and 100 per day, which is enough to build and test an agent and not enough to run a campaign.
For an agent handling a few hundred conversations a month, the email line is a rounding error next to the model tokens. The cost that matters on this channel is a burned domain, which is why the validation step is worth more than it looks.
When email is the wrong channel
If the answer is one line and needed now, email is the wrong place. Nobody watches their inbox the way they watch WhatsApp, and a two-hour reply that is technically fast reads as slow.
If you need a decision in a conversation, use a channel with turn-taking. Email threads are asynchronous by design, and an agent that needs three clarifications will take three days to get them.
If the recipient is a consumer in Brazil, LATAM, India or Southeast Asia, they are on WhatsApp and your email is going to a tab they open on Tuesdays.
Email wins on long context, attachments, B2B, and anything that needs a record. Which is a lot, and it is the part agents are best at.
Keep reading
- How to build an AI agent: the loop, then deployed on a channel.
- AI agent frameworks compared: where email sits in the stack.
- Newsletter examples: the marketing side of the same domain setup.
- AI agents for WhatsApp, SMS and email: one agent, every channel.