AutoFlowLab
← Tutorials

AI Email Triage Automation: Sort Your Gmail Inbox With n8n

Build an n8n workflow that uses AI to classify every Gmail message, apply labels, draft replies for leads and support, and push urgent emails to Slack.

June 2, 2026 · intermediate · 1 hour setup

A shared inbox that gets 80–150 emails a day costs someone on your team roughly an hour of pure sorting: deciding what’s a sales lead, what’s a support request, what’s an invoice that needs to go to bookkeeping, and what can be ignored. Worse, the genuinely urgent email — the client threatening to churn, the payment failure — sits unread behind 40 newsletters.

This tutorial builds a workflow in n8n that watches your Gmail inbox and, within about a minute of every email arriving:

  1. Classifies it into one of five categories: sales_lead, support, invoice, newsletter, or urgent
  2. Applies the matching Gmail label so the inbox sorts itself
  3. Drafts a reply (saved to Drafts, never auto-sent) for sales leads and support requests
  4. Posts urgent emails to a Slack channel with a summary and a direct link to the thread

The classification runs on an LLM with strict JSON output, so the routing is deterministic even though the judgment is fuzzy. Total build time is about an hour if your Gmail and Slack credentials are already connected.

What you’re building

The workflow has four stages:

Gmail Trigger → AI Classification (strict JSON) → Switch (5 outputs)
   ├── sales_lead → Add Label → Draft Reply → Gmail Create Draft
   ├── support    → Add Label → Draft Reply → Gmail Create Draft
   ├── invoice    → Add Label
   ├── newsletter → Add Label
   └── urgent     → Add Label → Slack Message

The key design decision: the AI never sends anything. It labels, drafts, and notifies. A human clicks Send. That keeps the failure mode of a bad classification at “mildly annoying” instead of “we emailed a customer nonsense.”

Prerequisites

  • An n8n instance — n8n Cloud or self-hosted (Docker on a $5 VPS works fine). As of mid-2026, n8n Cloud starts around €24/month and self-hosting is free for internal use — check current pricing.
  • A Gmail or Google Workspace account with OAuth credentials set up in n8n (Google Cloud project with the Gmail API enabled)
  • An OpenAI or Anthropic API key (any model with reliable JSON output; a small/cheap model is plenty for classification)
  • A Slack workspace where you can create a bot

Create the five Gmail labels manually before you start: Triage/Sales, Triage/Support, Triage/Invoice, Triage/Newsletter, Triage/Urgent. The Gmail node references labels by ID, and it’s easier to pick existing ones from the dropdown.

Step 1: Gmail Trigger

  1. Add a Gmail Trigger node.
  2. Credential: your Gmail OAuth2 credential. When creating it in Google Cloud, the consent screen needs the gmail.modify scope — read-only won’t let you add labels or create drafts later.
  3. Event: Message Received
  4. Poll Times: Every Minute
  5. Under Filters, set Label Names or IDs to INBOX so you don’t re-process sent mail, and leave Include Spam and Trash off.
  6. In Options, enable Download Attachments: off (you don’t need them, and large attachments slow down executions).

Run the node once with Fetch Test Event to confirm you get a message payload with id, threadId, From, Subject, and a text or snippet field.

Step 2: AI classification with strict JSON output

Add an OpenAI node (or Anthropic node — the setup is identical) right after the trigger.

  1. Resource: TextOperation: Message a Model
  2. Model: a fast, cheap model — classification doesn’t need a frontier model
  3. Simplify Output: on
  4. Enable the Output Content as JSON option (on the OpenAI node this sets response_format: json_object, which forces valid JSON)

In the Messages section add a System message and a User message. The system message is the full prompt below — paste it exactly:

You are an email triage classifier for a small business. Classify the email into exactly one category and return ONLY a JSON object, no markdown, no commentary.

Categories:
- "sales_lead": someone asking about buying our product/services, requesting a quote, demo, or pricing, or a warm referral introduction.
- "support": an existing customer with a question, problem, bug report, or how-to request.
- "invoice": an invoice, receipt, billing statement, payment confirmation, or accounting document — incoming or outgoing.
- "newsletter": marketing emails, newsletters, product announcements from vendors, event invitations, cold outreach trying to sell US something.
- "urgent": anything requiring action within hours — outage reports, angry customers threatening to cancel, payment failures, legal or security notices, a deadline today.

Rules:
1. "urgent" overrides every other category. An angry support email is "urgent", not "support".
2. Cold outbound sales emails sent TO us are "newsletter", not "sales_lead". A sales_lead is someone who wants to BUY from us.
3. If genuinely ambiguous, prefer "support" for known customers and "newsletter" for unknown senders.

Return this exact JSON shape:
{
  "category": "sales_lead" | "support" | "invoice" | "newsletter" | "urgent",
  "confidence": 0.0-1.0,
  "reason": "one short sentence",
  "summary": "one-sentence summary of what the sender wants"
}

The User message carries the email data as an expression:

From: {{ $json.From }}
Subject: {{ $json.Subject }}
Body:
{{ ($json.text || $json.snippet || '').slice(0, 4000) }}

Truncating the body to 4,000 characters matters: long email threads blow up token costs and add nothing to classification accuracy — the first few paragraphs almost always contain the signal.

Step 3: Validate the JSON before routing

LLMs with forced JSON mode are reliable but not perfect, and a workflow that crashes on email #3,000 at 2 a.m. is worse than one that quietly defaults. Add a Code node named Validate Classification:

const allowed = ['sales_lead', 'support', 'invoice', 'newsletter', 'urgent'];
let out;
try {
  const raw = $input.first().json.message?.content ?? $input.first().json.content;
  out = typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch (e) {
  out = {};
}
if (!allowed.includes(out.category)) {
  out.category = 'support';        // safe default: a human will see it
  out.confidence = 0;
  out.reason = 'fallback: invalid model output';
  out.summary = '';
}
// carry the original email fields forward
const email = $('Gmail Trigger').first().json;
return [{ json: { ...out, emailId: email.id, threadId: email.threadId,
  from: email.From, subject: email.Subject } }];

This also flattens the data so every downstream node gets category, summary, emailId, and threadId without digging through nested structures.

Step 4: Switch node

Add a Switch node:

  1. Mode: Rules
  2. Routing value: {{ $json.category }}
  3. Add five rules, one per category, each with Operation: Equals and the exact strings sales_lead, support, invoice, newsletter, urgent. Rename each output to match.
  4. Set Fallback Output to the support branch — belt and suspenders on top of the Code node default.

Step 5: Label every branch

Every one of the five branches starts with a Gmail node configured the same way:

  1. Resource: MessageOperation: Add Label
  2. Message ID: {{ $json.emailId }}
  3. Label Names or IDs: pick the matching label from the dropdown (Triage/Sales for sales_lead, and so on)

For the newsletter branch, optionally add a second operation to remove the INBOX label — that archives newsletters entirely so they never appear in the inbox, only under the label. Be careful enabling this before you’ve watched the classifier run for a week.

Step 6: Draft replies for sales leads and support

On the sales_lead and support branches, after the label node, add another OpenAI node. This one writes prose, so JSON mode stays off. System prompt for the sales branch:

You write first-draft replies for a small business. A human will edit and send this — write it ready-to-send but never promise specific pricing, dates, or features.

Tone: friendly, direct, no corporate filler. 80-140 words. No subject line, body only.

Structure:
1. Thank them and reflect back what they asked for in one sentence (proves a human-quality read).
2. One or two clarifying questions OR a concrete next step (a 20-minute call, a link to book time).
3. Sign off as "{{YOUR_NAME}}".

If the email does not actually contain a purchasable request, write a polite one-line ask for more detail instead.

The user message: Reply to this email:\n\nFrom: {{ $json.from }}\nSubject: {{ $json.subject }}\nSummary: {{ $json.summary }} — and pull the full body from the trigger with {{ $('Gmail Trigger').first().json.text }} if you want richer drafts.

For the support branch, swap the structure: acknowledge the issue, state that you’re looking into it, ask for one piece of missing diagnostic info (account email, screenshot, steps to reproduce), and never claim the issue is fixed.

Then add a Gmail node on each branch:

  1. Resource: DraftOperation: Create
  2. Subject: Re: {{ $json.subject }}
  3. Message: the LLM output expression
  4. In Options, set Thread ID: {{ $json.threadId }} — this attaches the draft to the existing conversation, so when you open the thread in Gmail the draft is sitting at the bottom ready to edit and send.

Step 7: Urgent emails to Slack

On the urgent branch, after the label node, add a Slack node:

  1. Resource: MessageOperation: Send
  2. Channel: #inbox-urgent (create it; don’t dump these into a busy channel)
  3. Message Text:
:rotating_light: *Urgent email* from {{ $json.from }}
*Subject:* {{ $json.subject }}
*Summary:* {{ $json.summary }}
*Open:* https://mail.google.com/mail/u/0/#inbox/{{ $json.threadId }}

The Slack bot needs the chat:write scope and must be invited to the channel (/invite @yourbot) — forgetting the invite is the single most common reason this node fails with channel_not_found.

Free template · n8n

AI Email Triage

email-triage-n8n.json

Download JSON

Import the template, swap in your three credentials (Gmail, LLM, Slack), re-pick the label IDs in the five Gmail nodes, and you’re 90% done.

Testing before you trust it

Don’t activate the workflow yet. Instead:

  1. Send yourself five test emails, one obviously matching each category, from another account.
  2. Use Fetch Test Event on the trigger and step through each execution manually.
  3. Then activate it and run it in label-only mode for a few days — disconnect the draft and Slack branches and just watch the labels. Skim the Triage/* labels each evening. When the misclassification rate feels acceptable (in practice: 90%+ with the prompt above), reconnect the action branches.

The confidence score gives you a tuning knob: add an IF node after the validation step that routes anything with confidence < 0.6 to the support branch regardless of category, so low-certainty calls always get human eyes.

Try it yourself

n8n

Self-host n8n and this entire workflow runs for pennies a day in LLM tokens — no per-task fees.

Start with n8n

Common errors and fixes

Insufficient Permission from the Gmail node. Your OAuth credential was created with read-only scope. Recreate the Google Cloud consent screen with https://www.googleapis.com/auth/gmail.modify, then reconnect the credential in n8n and re-authorize. Existing tokens don’t gain scopes retroactively.

LLM returns markdown-wrapped JSON (```json ... ```). This happens when JSON mode isn’t actually enabled — the checkbox in the OpenAI node, or a model that ignores response_format. The Code node in Step 3 catches it, but fix the root cause: confirm the JSON output option is on, and make sure the word “JSON” appears in your prompt (OpenAI’s API requires it when json_object mode is set).

429 Too Many Requests from the LLM API. A burst of 30 emails (Monday morning) hits the trigger at once and n8n fires 30 parallel classification calls. Open the OpenAI node settings, enable Retry On Fail with 3 retries and a 5-second wait. For sustained volume, set the workflow’s execution mode to queue, or add a Loop Over Items node with batch size 5.

Gmail Trigger silently stops after weeks of working. Google expires refresh tokens for OAuth apps left in “Testing” status after 7 days, and occasionally on password changes. Move your Google Cloud app to “In production” status. Also check n8n’s execution list for 401 errors — re-authorizing the credential takes 30 seconds.

Slack node fails with channel_not_found. The bot isn’t in the channel. /invite @yourbot in Slack. If the channel is private, the bot additionally needs the groups:write scope.

Drafts appear as new threads instead of replies. You set the Thread ID in the wrong field or passed the message ID instead. It must be threadId from the trigger payload, set in the draft node’s Options → Thread ID.

Zapier vs Make vs n8n for email triage

This is an n8n-shaped problem. The workflow runs on every single inbound email — on Zapier’s per-task pricing, 100 emails a day × 4–6 steps each is 12,000–18,000 tasks a month, which lands you in expensive plan territory fast (as of mid-2026, check current pricing). Make is meaningfully cheaper per operation and its router is a fine substitute for the Switch node, so it’s a reasonable middle ground if you want hosted-only. But n8n’s Code node for JSON validation, free self-hosted executions, and native LLM nodes make it the best fit for high-volume, always-on triage. See our full Make vs Zapier vs n8n comparison for the broader decision.

What this costs to run

With a small model doing classification at 1,500 input tokens per email, 100 emails/day costs a few cents daily in API fees. The draft-writing calls add a bit more. Self-hosted n8n adds server cost ($5–10/month); n8n Cloud or Make adds their subscription. As of mid-2026, the all-in cost for a self-hosted setup is typically under $15/month — check current pricing on each platform.

Where to go next

The classifier output is a routing signal you can reuse: pipe sales_lead emails into your CRM automatically with the pattern from our lead capture to CRM tutorial, send the invoice branch into the extraction workflow from automated invoice processing, and if support volume justifies it, upgrade the support branch from “draft a reply” to a full AI customer support bot with a knowledge base behind it.