|

n8n Email Triage Automation: Let Claude Sort Your Inbox

Disclosure: This post contains affiliate links. If you sign up for n8n via my link I may earn a commission — at no extra cost to you. I only recommend tools I actually use to run alexanderharte.com.


Part of The Automation Playbook series — 5 systems to reclaim 10+ hours a week. Download the free PDF guide here.


n8n email triage automation: stop sorting your inbox manually

The average entrepreneur spends 28% of their working day on email. Not doing deep work. Not building. Not creating. Sorting, labelling, drafting replies to the same kinds of messages over and over again.

System 05 of The Automation Playbook fixes that. This is the AI email triage assistant — a workflow that runs inside n8n, watches your Gmail inbox, and the moment a new email lands it sends it straight to Claude to be classified, labelled, and replied to — before you’ve even opened your laptop.

By the time you check your inbox:

  • Every email already has a Gmail label (Urgent, Sales Lead, Support, Newsletter, Spam)
  • Urgent emails and Sales Leads have fired a Telegram alert to your phone
  • Claude has written a draft reply in your voice — ready to review and send in seconds

You’re not eliminating email. You’re making yourself ruthlessly efficient with it.

Here’s exactly how to build it.

n8n email triage automation
The complete System 05 AI Email Triage workflow in n8n — Gmail Trigger, Claude classification, and 5 routing branches for Urgent, Sales Lead, Support, Newsletter, and Spam.

What you’ll build

A fully automated email triage workflow that:

  • Polls your Gmail inbox every minute for unread emails
  • Sends each email to Claude (via the Anthropic API) for AI classification
  • Routes the email into one of five categories: Urgent, Sales Lead, Support, Newsletter, or Spam
  • Applies the correct Gmail label automatically
  • Saves a Claude-written draft reply to Gmail Drafts for Urgent, Sales Lead, and Support emails
  • Fires a Telegram notification for Urgent and Sales Lead emails only

System overview

StepNodeWhat happens
1. TriggerGmail TriggerPolls for new unread emails every minute. Passes From, Subject, and snippet to next node.
2. ClassifyHTTP Request → Anthropic APISends email content to Claude. Returns a JSON object with category, summary, and a draft reply.
3. ParseCode nodeExtracts Claude’s JSON response. Handles markdown fences in case Claude wraps the output.
4. RouteSwitch nodeRoutes to one of 5 branches based on the category Claude returned.
5a. UrgentGmail + TelegramLabels IMPORTANT, saves draft reply, fires Telegram alert 🚨
5b. Sales LeadGmail + TelegramLabels IMPORTANT, saves draft reply, fires Telegram alert 💰
5c. SupportGmailLabels as Personal, saves draft reply. No alert — review when ready.
5d. NewsletterGmailLabels as Promotions. No draft, no alert.
5e. SpamGmailMarked as Spam. Done.

What you’ll need before you start

  • n8n — self-hosted (I run mine on AWS Lightsail via Docker Compose) or n8n Cloud. Either works.
  • Gmail account — with OAuth2 credentials set up in n8n
  • Anthropic API key — get yours at console.anthropic.com
  • Telegram account — for notifications (free). You’ll create a bot via @BotFather.

Estimated build time: 45–60 minutes including credential setup.

Ongoing cost: Claude API calls are roughly $0.01–$0.03 per email depending on length. For a typical inbox of 50–100 emails per day, you’re looking at under $1/day.


Step 1 — Set up the Gmail Trigger

The Gmail Trigger node polls your inbox on a schedule. Out of the box it runs every minute — which is fine for most inboxes. You can dial it back to every 5 or 15 minutes in the node settings if you want to reduce API calls.

Configure the node

  1. Add a Gmail Trigger node to your canvas
  2. Set Authentication to OAuth2 and connect your Gmail credential
  3. Set Event to Message Received
  4. Under Filters, set Read Status to Unread only
  5. Leave Simplify toggled on — this gives you a clean output with just the fields you need

What the Gmail Trigger actually outputs

This tripped me up during the build. With Simplify enabled, the Gmail Trigger outputs capitalised field names — not lowercase. The fields you’ll use downstream are:

  • From — the sender’s name and email address
  • Subject — the email subject line
  • snippet — a short preview of the email body (lowercase, the exception)
  • id — the Gmail message ID
  • threadId — the thread ID (used for saving draft replies into the correct thread)

Getting these field names right matters. If you reference $json.from instead of $json.From you’ll get undefined and Claude will classify every email as Spam.

Gmail Trigger configured to poll for unread emails only. Note the capitalised field names in the output panel — From and Subject — these must match exactly in any downstream expressions.

Step 2 — Connect Claude via the Anthropic API

This is the brain of the system. We’re using an HTTP Request node to call the Anthropic API directly — this gives you full control over the model, system prompt, and response format without any abstraction layer getting in the way.

Configure the HTTP Request node

  1. Add an HTTP Request node
  2. Set Method to POST
  3. Set URL to https://api.anthropic.com/v1/messages
  4. Set Authentication to Predefined Credential TypeAnthropic and connect your Anthropic API credential
  5. Add these Headers:
    • anthropic-version: 2023-06-01
    • content-type: application/json
  6. Set Body to JSON and paste in the payload below

The Claude prompt

The system prompt is doing the heavy lifting here. It defines the five categories, tells Claude to respond in JSON only, and specifies the exact format — which makes parsing the response reliable downstream.

{
  "model": "claude-opus-4-6",
  "max_tokens": 1024,
  "system": "You are an email triage assistant for Alex Harte, a solopreneur who runs alexanderharte.com — a blog and content business focused on AI automation for solopreneurs. Your job is to classify incoming emails and, where appropriate, draft a short reply in Alex's voice: practical, direct, no fluff.\n\nClassify each email into EXACTLY one of these categories:\n- Urgent: requires immediate attention (time-sensitive issues, payment problems, critical errors)\n- Sales Lead: potential customer, affiliate inquiry, partnership opportunity, sponsorship\n- Support: reader question, tool help request, content question\n- Newsletter: email marketing, digest emails (not requiring a reply)\n- Spam: promotional, irrelevant, cold outreach with no relevance\n\nRespond ONLY with valid JSON in this exact format:\n{\n  \"category\": \"<one of: Urgent|Sales Lead|Support|Newsletter|Spam>\",\n  \"summary\": \"<one sentence, max 120 chars>\",\n  \"draft_reply\": \"<a short reply in Alex's voice — leave blank string if Newsletter or Spam>\"\n}",
  "messages": [
    {
      "role": "user",
      "content": "From: {{ $json.From }}\nSubject: {{ $json.Subject }}\n\n{{ $json.snippet }}"
    }
  ]
}

Customise the system prompt for your business. Replace “Alex Harte” with your name, update the business description, and adjust the category definitions to match the kinds of email your inbox actually receives. The more specific you are here, the more accurate Claude’s classification will be.

The HTTP Request node sending email content to Claude via the Anthropic API. The JSON body contains the model, system prompt defining the five categories, and the dynamic message built from the Gmail Trigger output.

Step 3 — Parse Claude’s response

Claude returns a JSON object inside a text content block. The Code node extracts that JSON and packages it for use downstream — and handles edge cases where Claude wraps the output in markdown fences (which it sometimes does).

The Parse code

// HTTP Request node returns Anthropic response in $json.content[0].text
const raw = ($json.content?.[0]?.text || '').trim();

let parsed;
try {
  parsed = JSON.parse(raw);
} catch(e) {
  // Strip markdown fences if Claude added them
  const match = raw.match(/\{[\s\S]*\}/);
  try {
    parsed = JSON.parse(match ? match[0] : '{}');
  } catch(e2) {
    parsed = {};
  }
}

return [{
  json: {
    category: parsed.category || 'Support',
    summary: parsed.summary || '',
    draft_reply: parsed.draft_reply || '',
    message_id: $('Gmail Trigger').first().json.id || '',
    thread_id: $('Gmail Trigger').first().json.threadId || ''
  }
}];

Set the Code node to Run Once for All Items mode. This is important — in per-item mode, node references like $('Gmail Trigger') don’t resolve correctly.

Notice that draft_reply defaults to an empty string rather than undefined. This matters — if it arrives as undefined at the Gmail draft node, you’ll get a cryptic “Cannot read properties of undefined (reading ‘trim’)” error. The fallback prevents that.

The Code node successfully parsing Claude’s JSON response. The output panel shows a correctly classified Sales Lead complete with a draft reply written in Alex’s voice — ready to save to Gmail Drafts.

Step 4 — Route by category

The Switch node reads the category field from the Parse node and routes the execution to one of five output branches. Each output is named after its category — Urgent, Sales Lead, Support, Newsletter, Spam — which makes the workflow easy to read at a glance.

Configure the Switch node

  1. Add a Switch node and set Mode to Rules
  2. Add five rules, one per category
  3. For each rule: Left Value = {{ $json.category }}, Operator = equals, Right Value = the category name (e.g. Urgent)
  4. Enable Rename Output on each rule and set the output key to match the category name

This gives you five named outputs coming out of the Switch node — one wire per category running down to its own branch.

The Switch node configured with five named outputs — Urgent, Sales Lead, Support, Newsletter, and Spam. Each output wire runs to its own independent branch.

Step 5 — Build the five branches

Each branch follows the same pattern: label the email in Gmail, optionally save a draft reply, optionally fire a Telegram notification. Here’s what each branch does and how to configure it.

🔴 Urgent branch

Gmail — Add Label node: Set Resource to Message, Operation to Add Labels, Message ID to {{ $json.message_id }}, and Label to IMPORTANT.

Gmail — Create Draft node: Set Resource to Draft, Operation to Create. Then:

  • To: {{ $('Parse Claude Response').first().json.original_from }}
  • Subject: Re: {{ $('Parse Claude Response').first().json.original_subject }}
  • Message: {{ $('Parse Claude Response').first().json.draft_reply }}
  • Thread ID (under Options): {{ $('Parse Claude Response').first().json.thread_id }}

Important: Reference the Parse node directly using $('Parse Claude Response').first().json — not $json. The Label node overwrites $json with its own API response, so $json.draft_reply will be undefined by the time the Draft node runs.

Telegram node: Send a message to your personal chat with the sender, subject, and Claude’s summary. Reference Parse directly here too for the same reason.

🟠 Sales Lead branch

Identical to Urgent — label IMPORTANT, save draft, fire Telegram alert. The only difference is the Telegram message text and emoji.

🔵 Support branch

Label as CATEGORY_PERSONAL and save a draft reply. No Telegram alert — these can wait until you’re ready to work through them.

⚪ Newsletter branch

Label as CATEGORY_PROMOTIONS. No draft, no alert. Gmail files it away automatically.

⚫ Spam branch

Label as SPAM. Gmail handles the rest.

The Urgent branch: Label as Important → Save Claude’s draft reply to Gmail Drafts → Fire a Telegram alert. The Sales Lead branch follows the same pattern with a different notification message.

How to set up your Telegram bot

This is the part most tutorials skip over. Here’s the exact process.

1. Create the bot via @BotFather

  1. Open Telegram and search for @BotFather
  2. Send /newbot
  3. Give it a display name — e.g. Alex Harte Alerts
  4. Give it a username — must end in bot, e.g. alexharte_alerts_bot
  5. BotFather gives you a bot token — looks like 7412836490:AAFxxx... — copy it

2. Get your personal Chat ID

You can’t just drop a bot token in and start receiving messages — Telegram needs to know which chat to send to. The easiest way to get your Chat ID:

  1. Go to your bot in Telegram and send it any message (just type hi)
  2. Paste this URL into your browser, replacing YOUR_BOT_TOKEN with the full token BotFather gave you:
    https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates
  3. In the JSON response, find the "chat" object and copy the "id" value — that number is your Chat ID

Common mistake: The word bot in the URL is joined directly to the token — no space, no slash. It’s /bot7412836490:AAF... not /bot/7412836490:AAF.... Also make sure you send your bot a message before hitting the URL — getUpdates returns nothing if there’s been no activity.

3. Add the credential in n8n and start the bot

  1. In n8n → Credentials → Add → search Telegram → paste the bot token → Save
  2. Assign the credential to both Telegram nodes in the workflow
  3. Paste your Chat ID into the Chat ID field on both nodes
  4. Back in Telegram, find your bot by username and tap Start — the bot won’t deliver messages to a chat that’s never been opened

Things that will trip you up (and how to fix them)

I hit all of these during the build. Saving you the debugging time.

“Node is not installed” on the Claude node

This happens if you try to use a node type that isn’t available in your n8n version. The fix: use an HTTP Request node calling the Anthropic API directly (as shown in Step 2) rather than any community Anthropic node. The HTTP Request node is always available and gives you full control.

Claude classifies everything as Spam

Nine times out of ten this means the email content isn’t reaching Claude. Check what’s actually being sent in the HTTP Request node body. The Gmail Trigger uses capitalised field names — From and Subject — not lowercase. If your JSON body references $json.from or $json.subject you’ll get empty strings and Claude will see a blank email.

“Cannot read properties of undefined (reading ‘trim’)” on the Draft node

This means draft_reply is arriving as undefined at the Gmail node. There are two causes:

  • The Parse node isn’t outputting draft_reply correctly — check the Code node output in the execution panel
  • You’re referencing $json.draft_reply but the Label node upstream has overwritten $json with its own response. Fix: reference the Parse node directly with $('Parse Claude Response').first().json.draft_reply

Telegram From/Subject fields are blank in the notification

Same root cause as above — by the time Telegram runs, $json has been overwritten by the Draft node’s response. Reference the Gmail Trigger directly: $('Gmail Trigger').first().json.From and $('Gmail Trigger').first().json.Subject.

getUpdates returns an empty result

You haven’t sent your bot a message yet. Open Telegram, find your bot, send it anything — even just “hi” — then refresh the getUpdates URL. The Chat ID will appear in the response.


What does this cost to run?

ComponentCost
n8n (self-hosted on AWS Lightsail)~$7/month (shared with all other workflows)
Anthropic API (Claude Opus)~$0.01–$0.03 per email. ~$15–$45/month for 50 emails/day
Telegram botFree
Gmail APIFree

If cost is a concern, swap claude-opus-4-6 for claude-haiku-4-5-20251001 in the HTTP Request body. Haiku is significantly cheaper and still performs well for straightforward classification tasks — you’ll lose some nuance in the draft replies but the routing accuracy holds up.


What to do once it’s live

Run it for a week before fully trusting it. Check the Gmail Drafts folder daily and review how Claude is classifying. You’ll probably spot a few edge cases — emails that should be Support but are landing in Spam, or newsletters being misclassified as Sales Leads. When that happens, tighten the system prompt category definitions with specific examples.

Once you’re confident in the classification accuracy, the only thing left to do is review Drafts and hit send. The sorting, labelling, and first-draft writing is handled.

That’s 28% of your working day back.


The rest of The Automation Playbook

This is System 05 of five. If you haven’t built the others yet:

Or download the full Automation Playbook PDF to get all five systems in one place.


Alex Harte | alexanderharte.com

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *