Build an n8n Email Agent With a Real Inbox

9 min read

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

John Joubert

Founder, Robotomail

Build an n8n Email Agent With a Real Inbox
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_key stored 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