teachyou.ai academy
← All posts
Workflow Automationn8nemail automationAI agentsIMAP

Automating Email Triage with AI in n8n

Pramod Dutta · Jun 26, 2026 · 12 min read

If your inbox is where deals go to die, an n8n email ai workflow can fix that in an afternoon. n8n is an open source workflow automation tool with a visual canvas, and when you wire its email trigger nodes to an AI model, you get a triage system that reads every incoming message, decides what it is, and routes it before a human ever opens the inbox. This guide builds that system end to end: connect a mailbox, classify messages with an AI node, branch on the result, and take real actions like labeling, Slack alerts, and CRM ticket creation.

Email triage is a good first automation project because the inputs are messy (subject lines, forwarded threads, attachments, spam) and the payoff is immediate: fewer missed leads, faster support replies, less manual sorting. n8n is a strong fit because it is self-hostable, has native nodes for IMAP, Gmail, and Outlook, and ships with an AI Agent node plus HTTP Request node for any model provider you want to call.

Why n8n for email automation

n8n sits between no-code tools like Zapier and full custom code. You get a drag-and-drop canvas, but every node exposes an expression editor where you can write JavaScript, so you are never boxed in by a missing integration. For email specifically, three things matter:

  • Native triggers: the Email Trigger (IMAP) node polls a mailbox and fires a new execution per message, without you managing webhooks or OAuth token refresh logic yourself for basic IMAP accounts.
  • First-class AI nodes: the AI Agent and Basic LLM Chain nodes handle prompt templating, structured output parsing, and conversation memory, so you are not hand-rolling JSON parsing for every model response.
  • Self-hosting: you can run n8n in Docker on your own infrastructure, which matters if the emails contain customer PII you do not want passing through a third-party SaaS automation platform.

The workflow we are building has five stages: trigger, extract, classify, route, act. Each stage is one or more nodes on the canvas.

Setting up the email trigger

Start with a mailbox connection. For Gmail, use the Gmail Trigger node with OAuth2 credentials scoped to read-only access if you only need triage (add send scope later if you want auto-replies). For any other provider, the Email Trigger (IMAP) node works with a host, port, and app password.

  1. Add an Email Trigger (IMAP) node to a new workflow.
  2. Set the mailbox to a dedicated triage inbox, not your primary inbox, while you test. A shared alias like triage@yourdomain.com with a filter rule that copies mail into it works well.
  3. Set Post Processing Action to Mark as Read only after your workflow finishes successfully, not on trigger. Leave it as Nothing during development so you can rerun the same email multiple times.
  4. Set the polling interval to something reasonable like every minute. IMAP polling is lightweight, but do not go below 30 seconds unless you have a real volume reason to.

Run the node once manually with Execute Node on a test email already in the inbox to confirm you get a JSON payload with subject, from, text, html, and attachments fields. This payload is what every downstream node will reference.

Extracting a clean signal from the raw email

Raw email JSON is noisy: HTML wrapper markup, quoted reply chains, signature blocks, tracking pixels. Feeding all of that into an AI model wastes tokens and can confuse classification. Add a Code node right after the trigger to normalize the payload before it reaches the AI step.

const item = $input.item.json;

// Prefer plain text, fall back to a stripped version of html
let body = item.text || item.html.replace(/<[^>]+>/g, ' ');

// Trim to the first reply, drop quoted history
const replyMarkers = [
  /On .+ wrote:/,
  /-----Original Message-----/,
  /From: .+\nSent: .+/
];
for (const marker of replyMarkers) {
  const match = body.match(marker);
  if (match) {
    body = body.slice(0, match.index);
  }
}

body = body.replace(/\s+/g, ' ').trim().slice(0, 4000);

return {
  json: {
    from: item.from.text || item.from,
    subject: item.subject || '(no subject)',
    body,
    hasAttachments: (item.attachments || []).length > 0,
    receivedAt: item.date
  }
};

Capping the body at 4000 characters is deliberate. Classification does not need the full thread, and shorter prompts are cheaper and faster to run through an AI model.

Classifying the email with an AI node

This is where the "ai" in n8n email ai happens. Add a Basic LLM Chain node (or the AI Agent node if you want the model to be able to call tools mid-classification, which is overkill for pure triage). Connect it to a Chat Model sub-node pointing at whichever provider you use.

Set the prompt so the model returns structured output you can branch on reliably. Do not ask for free-text classification; ask for a fixed enum, because your downstream Switch node needs an exact string match.

You are an email triage assistant for a small SaaS company.
Classify the email below into exactly one category:
- sales_lead: a prospect asking about pricing, a demo, or the product
- support_request: an existing customer reporting a bug or asking how to do something
- billing: invoice, payment, refund, or subscription questions
- spam: unsolicited marketing, phishing, or irrelevant content
- other: anything that does not fit the above

Also assign urgency as one of: low, normal, high.
High urgency means the customer mentions an outage, data loss, or says they are cancelling.

Respond with only valid JSON in this exact shape:
{"category": "...", "urgency": "...", "summary": "one sentence summary"}

Subject: {{ $json.subject }}
From: {{ $json.from }}
Body: {{ $json.body }}

Enable the node's structured output option if your n8n version supports it, or add a downstream Code node that does JSON.parse() on the model's text response wrapped in a try/catch, falling back to category: "other" if parsing fails. Never let a malformed model response crash the whole workflow, because that email disappears from triage instead of landing in a fallback bucket.

let parsed;
try {
  const raw = $input.item.json.text || $input.item.json.output;
  parsed = JSON.parse(raw.trim());
} catch (e) {
  parsed = { category: 'other', urgency: 'normal', summary: 'Could not classify automatically' };
}

return {
  json: {
    ...$('Normalize Email').item.json,
    ...parsed
  }
};

Notice the spread of the earlier "Normalize Email" node's output alongside the classification result. You want the routing and action stages to have both the original email data and the AI verdict in one object.

Routing with a Switch node

Add a Switch node keyed on {{ $json.category }} with one output per category: sales_lead, support_request, billing, spam, other. This is where triage becomes action.

For spam: connect straight to a Gmail node action that applies a "Filtered" label and archives the message. No human involvement needed.

For sales_lead: connect to a node that creates a record in your CRM (HubSpot, Pipedrive, or a simple Airtable base) via that service's dedicated n8n node, then a Slack node that posts to a #sales-leads channel with the summary and urgency.

New lead: {{ $json.from }}
Subject: {{ $json.subject }}
Summary: {{ $json.summary }}
Urgency: {{ $json.urgency }}

For support_request: create a ticket in your helpdesk tool (Zendesk, Freshdesk, or a Notion database if you run something lighter) and, if urgency is high, add a second branch with an IF node that pages someone directly through a Slack DM or an on-call tool's webhook.

For billing: route to whoever owns billing, typically a Slack channel plus a tag applied back on the email itself so a human sees the classification when they open the thread.

For other: label it and leave it in the inbox for manual review. Do not try to force every email into a confident bucket; a well-designed triage system has a deliberate escape hatch.

Writing the classification back to the mailbox

Whatever the branch, close the loop by labeling the original email so a human scanning the inbox visually sees the triage result without opening your automation logs. The Gmail node's Add Label operation (or IMAP's move-to-folder equivalent) takes the message ID from the original trigger output, which is why keeping that data flowing through every downstream node matters.

If you are on Gmail, create labels ahead of time: AI/Sales, AI/Support, AI/Billing, AI/Spam, AI/Other. Reference the message ID with an expression like {{ $('Email Trigger').item.json.id }} since the Switch and Code nodes in between do not automatically preserve every original field unless you explicitly carry it forward, as shown in the normalize step above.

Handling attachments and edge cases

Attachments deserve a special case because most classification prompts should not include raw file contents. Check hasAttachments in your Switch node logic: if support_request and hasAttachments is true, that is very likely a screenshot of a bug, so route it to a higher-priority support lane automatically, even if the AI model rated urgency as normal. Small heuristics layered on top of the AI classification catch cases the model alone would miss.

Also handle:

  • Auto-replies and bounces: filter these out before they hit the AI node with a Code node that checks for headers like Auto-Submitted or subject lines starting with "Out of Office" or "Delivery Status Notification." Running an AI call on every bounce message wastes API spend.
  • Non-English email: if you serve a global audience, either instruct the model explicitly to classify regardless of language (most current models handle this fine) or add a language-detection step first and route non-English mail to a human-language-appropriate queue.
  • Duplicate threads: if the same sender emails twice within a short window, add a dedupe check keyed on the sender address and a time window using a Wait node or a lightweight database lookup (Postgres, Redis, or even n8n's built-in data tables) so you do not create two CRM leads for one conversation.

Testing before going live

Do not point this workflow at a production support inbox on day one. Build a test dataset of 20 to 30 real (or realistic) emails covering every category, save them as static JSON in an n8n Set node you can swap in for the trigger, and run the whole workflow manually against each one. Check three things per test case:

  1. The category and urgency match what a human would assign.
  2. The routing action (Slack message, CRM record, label) actually fires with correct data.
  3. The workflow does not throw on edge cases: empty body, no subject, HTML-only email, attachment-only email.

Once accuracy looks solid, switch the trigger's Post Processing back to Mark as Read on success, point it at the real triage inbox, and watch the n8n execution log for the first day rather than assuming it works.

Monitoring and cost control

Every AI node execution costs tokens, so add basic guardrails:

  • Cap the body text sent to the model (the 4000-character trim above) so a single massive email thread does not blow up your per-request cost.
  • Use n8n's built-in Error Trigger workflow to catch failures anywhere in the triage workflow and post them to a monitoring channel, so a broken IMAP credential does not silently stop triage for days.
  • Track execution count and average token usage weekly. If volume grows, consider a cheaper, faster model for the classification step specifically, since triage is a narrow, well-defined task that does not need your most capable (and most expensive) model.
  • Set a monthly budget alert with your AI provider so a runaway loop (for example, a Switch misconfiguration that re-triggers the same email) cannot silently rack up spend.

Extending the workflow

Once basic triage is stable, natural next steps include:

  • Draft auto-replies: for support_request category with urgency: low, have the AI Agent node draft a suggested reply and post it to Slack for a human to approve and send, rather than sending automatically. Keep a human in the loop for anything customer-facing until you trust the system.
  • Sentiment tracking: add a second AI call (or extend the same prompt) to score sentiment, and feed that into a weekly report so you can see if support tone is trending negative before it shows up in churn numbers.
  • Multi-mailbox support: duplicate the trigger for a sales alias and a support alias, both feeding into the same classification and routing sub-workflow using n8n's Execute Workflow node, so you maintain one classification prompt instead of copies scattered across workflows.

FAQ

Does this require a paid n8n plan? No. n8n's self-hosted community edition, run in Docker or via npm, supports every node used here: Email Trigger, Gmail, Code, AI Agent, Basic LLM Chain, Switch, and Slack. A paid n8n Cloud plan only becomes relevant if you want managed hosting instead of running your own instance.

Which AI model should I use for classification? Any current chat-capable model works, since email triage is a well-bounded classification task, not open-ended reasoning. Pick based on cost and latency: a smaller, faster model keeps this workflow cheap to run at high email volume, while a larger model helps if your categories are subtle or your emails are long and ambiguous. Test both against your labeled dataset before committing.

What happens if the AI misclassifies an email? That is why every branch still writes a label back to the original email and, for ambiguous cases, routes to the other bucket instead of forcing a guess. Treat the AI classification as a fast first pass that a human can override by moving the email to a different label manually, not as a fully autonomous decision with no oversight.

Can I use this with Outlook instead of Gmail? Yes. Swap the Gmail Trigger and Gmail action nodes for the Microsoft Outlook Trigger and Outlook nodes, which n8n supports natively with OAuth2. The classification and routing logic in the middle of the workflow does not change at all, since it operates on the normalized JSON object, not provider-specific fields.

How do I prevent the workflow from processing the same email twice? Set the Email Trigger's Post Processing Action to mark messages as read (or move them to a processed folder) only after a successful run, and make sure that action sits at the very end of every branch, including the spam and error paths. If you need stronger guarantees, log processed message IDs into a database and check against that log at the start of the workflow before doing any AI classification work.

Is it safe to run customer emails through a third-party AI API? Check your AI provider's data retention and training policy before sending customer email content, and prefer a provider that does not train on API inputs by default. If you handle regulated data, consider a self-hosted model behind n8n's HTTP Request node instead of a hosted API, and always strip attachments and unnecessary PII in the normalize step before the AI call, not after.