Build an n8n Email Agent With a Real Inbox
Give your n8n AI agent its own email address: trigger workflows from inbound mail via webhook, send threaded replies, and run per-workflow mailboxes.
John Joubert
Founder, Robotomail

Table of contents
To build an n8n email agent that actually sends and receives mail, you need three things: a real mailbox with its own address, an inbound webhook that fires your n8n workflow when mail arrives, and an outbound call that sends the agent's reply back into the same thread. With Robotomail that is a mailbox create call, a webhook pointed at your n8n Webhook node's production URL, and an HTTP Request node hitting the messages endpoint.
No IMAP polling, no OAuth consent screens, no shared human inbox that your workflow has to compete with.
Why the Gmail and IMAP nodes fall short for agents
n8n ships an IMAP Email trigger and a Gmail node, and both work fine when a human owns the mailbox. They start to hurt when an autonomous workflow owns it:
- Polling latency and cost. The IMAP trigger polls. You either accept a delay or hammer the server every minute. Webhooks fire in the order events happen and cost nothing while idle.
- OAuth lifecycle. Google OAuth apps in testing mode expire refresh tokens, and per-agent Google Workspace seats get expensive fast. We wrote up the tradeoffs in more detail in our comparison with the Gmail API.
- One mailbox, many workflows. If ten n8n workflows share one Gmail account, you need routing rules to figure out which workflow owns which message. Separate mailboxes make ownership structural instead of conditional.
- State and threading. IMAP gives you flags and UIDs. It does not give you a stable thread identifier you can key a workflow's memory on.
The architecture
inbound mail
-> Robotomail mailbox (agent@yourdomain.com)
-> POST to n8n Webhook node (production URL)
-> Code node: normalize + dedupe
-> AI Agent node (chat model + tools)
-> HTTP Request node: POST reply with inReplyTo
-> Robotomail sends, thread stays intact
Everything runs inside one n8n workflow. The agent node never touches SMTP or IMAP. It reads a JSON object and writes a JSON object.
Step by step: wiring the mailbox to n8n
1. Create the mailbox
The CLI is the fastest path:
npx @robotomail/cli mailbox create support-triage
Or over HTTP:
curl -X POST https://api.robotomail.com/v1/mailboxes \
-H "Authorization: Bearer rm_your_api_key" \
-H "Content-Type: application/json" \
-d '{"address": "support-triage"}'
You get back a mailbox ID and a live address on the platform domain. If you want mail arriving at support-triage@yourcompany.com instead, verify a custom domain first and pass its domainId when you create the mailbox. The custom domain guide covers the DNS records.
2. Add the Webhook node and grab the production URL
In n8n, add a Webhook node as the trigger. Set:
- HTTP Method: POST
- Path: something specific, like
robotomail/support-triage - Respond: Immediately
That last setting matters. Robotomail expects a fast 2xx acknowledgement, not a response held open while a language model thinks for thirty seconds. Respond immediately, then let the rest of the workflow run asynchronously.
Copy the Production URL, not the Test URL. The test URL only listens while you have the editor open with "Listen for test event" active, which is useful for the first debugging pass and useless in production.
3. Register the webhook with Robotomail
Webhooks are their own resource in the API. Register your n8n production URL against the mailbox and you will start receiving message.received events. The exact request shape lives in the webhooks API reference; the conceptual model, including delivery retries, is in webhooks concepts.
4. Normalize the inbound payload
Every event arrives as { event, timestamp, data } with snake_case fields inside data:
{
"event": "message.received",
"data": {
"message_id": "...", "mailbox_id": "...", "mailbox_address": "support-triage@robotomail.co",
"from": "customer@example.com", "subject": "Re: order A-4521",
"body_text": "...", "thread_id": "...", "received_at": "2026-04-17T10:00:00.000Z"
}
}
Add a Code node right after the webhook to flatten it and drop anything you do not want the agent to act on:
const body = $input.first().json.body;
if (body.event !== 'message.received') {
return [];
}
const d = body.data;
return [{
json: {
messageId: d.message_id,
mailboxId: d.mailbox_id,
threadId: d.thread_id,
from: d.from,
subject: d.subject,
bodyText: (d.body_text || '').slice(0, 8000),
receivedAt: d.received_at,
},
}];
Returning [] ends the branch cleanly for events you do not handle. Truncating the body keeps a forwarded 200-message thread from blowing through your model's context window.
5. Feed the AI Agent node
Point an AI Agent node at the normalized item. Two things make the difference between a demo and something you can leave running:
Use threadId as the memory key. In the agent's memory settings, set the session key to {{ $json.threadId }}. Now every reply in a conversation resolves to the same memory window, and separate conversations stay isolated. That is the whole reason we expose a stable thread ID rather than making you reconstruct threading from References headers.
Constrain the output. Tell the system prompt to return the reply body only, no subject line, no "Here is a draft". Something like:
You are an email agent reading mail sent to support-triage@yourcompany.com.
Reply in plain text, under 200 words. Do not invent order details.
If you cannot resolve the request, say a human will follow up within one business day.
6. Send the reply
Add an HTTP Request node:
- Method: POST
- URL:
https://api.robotomail.com/v1/mailboxes/{{ $json.mailboxId }}/messages - Authentication: Header Auth,
Authorization: Bearer rm_your_api_keystored as an n8n credential - Body: JSON
{
"to": ["{{ $('Normalize').item.json.from }}"],
"subject": "Re: {{ $('Normalize').item.json.subject }}",
"bodyText": "{{ $json.output }}",
"inReplyTo": "{{ $('Normalize').item.json.messageId }}"
}
inReplyTo is the field that keeps the conversation stitched together in the recipient's client. Omit it and your reply lands as a brand new thread, which looks broken to whoever is reading it. More on how we build the headers is in threading concepts.
The equivalent raw curl, for testing outside n8n:
curl -X POST https://api.robotomail.com/v1/mailboxes/mbx_shopping/messages \
-H "Authorization: Bearer rm_your_api_key" \
-H "Content-Type: application/json" \
-d '{"to": ["vendor@example.com"], "subject": "Order #A-4521 delivery update?", "bodyText": "Hi, checking on the status of order A-4521."}'
Per-workflow mailboxes
The pattern that scales in n8n is one mailbox per workflow, not one mailbox per company. Each mailbox gets its own address, its own webhook URL, and its own audit trail.
A naming scheme that has held up for us:
| Workflow | Mailbox address | Webhook path |
|---|---|---|
| Inbound support triage | support-triage@ |
/robotomail/support-triage |
| Invoice follow-ups | billing-agent@ |
/robotomail/billing |
| Recruiting screener | apply@ |
/robotomail/apply |
| Vendor order checks | orders-agent@ |
/robotomail/orders |
Mailbox creation is one API call, so provisioning can itself be an n8n workflow. Spin up a mailbox per customer, per campaign, or per sub-agent, store the returned ID in your database, and register the webhook in the same run. That is the same primitive behind multi-agent workflows, where agents email each other and each one needs a distinct return address.
Idempotency, loops, and the failure modes that bite
Deduplicate on message_id. Webhook delivery is at-least-once. If your n8n instance returns a 500 or times out, the event is retried, and an agent that replies twice to the same email looks unhinged. Write message_id to a Postgres table or n8n's static data with a unique constraint and short-circuit on conflict, before the model runs.
Break reply loops. If your agent emails another automated address, you can end up in an infinite exchange. Two cheap guards: reject inbound mail whose from matches any of your own mailbox addresses, and count messages in the thread, halting past a threshold like 10. Check both in the normalize node.
Handle the no-reply case. Not every inbound message deserves an answer. Give the agent an explicit way to signal "no reply needed", and branch on it with an IF node instead of forcing a send.
Attachments are separate. Inbound attachment metadata is exposed through the attachments API rather than inlined in the webhook payload, which keeps event bodies small and predictable. Fetch them in a follow-up HTTP Request node only when the agent needs the contents. See attachments.
Set an error workflow. In workflow settings, assign an error workflow that posts to Slack or logs to a table. A silently failing email agent is worse than no email agent, because the sender assumes someone read their message.
FAQ
Can I use the n8n AI Agent node's tool calling to send email instead of a fixed HTTP Request node?
Yes. Attach an HTTP Request Tool pointed at the messages endpoint and describe it as "send an email reply". This is better when the agent may need to send several messages or contact a third party, and worse when you want deterministic single replies. Fixed node for triage, tool for anything conversational.
Do I need a custom domain?
No. Mailboxes work immediately on the platform domain, which is the right choice while prototyping. Move to a custom domain when the agent starts talking to customers, so the address matches your brand and your own DNS reputation carries the mail.
How do I test the workflow without sending real mail?
Use the Webhook node's Test URL and post a sample message.received payload with curl. That exercises your parsing, agent, and reply logic end to end. Then send one real email to the mailbox before switching to the production URL. Our notes on receiving and replying walk the full loop.
Does this work for self-hosted n8n behind a firewall?
The webhook target has to be reachable from the internet. If your n8n is internal, put a reverse proxy or tunnel in front of the webhook path, or have Robotomail post to a small public relay that forwards into your network. Everything else, including outbound sends, works from a private network.
Provision a mailbox, paste your n8n production webhook URL, and your agent has a working address in a few minutes. Start at robotomail.com, or read how to give your AI agent an email address for the framework-agnostic version of this setup.
Give your AI agent a real email address
One API call creates a mailbox with full send and receive. Webhooks for inbound, automatic threading, deliverability handled. 30-day money-back guarantee.
Related posts

AI Email Agent: Build One That Sends and Receives
What an AI email agent is, and how to build one with its own mailbox, inbound webhook, reply loop, and correct threading. With working code.
Read post
Gmail MCP Server for AI Agents: Setup and Limits
How a Gmail MCP server exposes tools like search_emails, how to wire it up, where OAuth and quotas break, and when an agent mailbox fits better.
Read post
Best Email APIs With Webhook Events for AI Agents
A webhook-first comparison of email APIs: inbound events, payload shape, signing, retries, ordering and replay for agents that react to incoming mail.
Read post