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.

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
| Step | Node | What happens |
|---|---|---|
| 1. Trigger | Gmail Trigger | Polls for new unread emails every minute. Passes From, Subject, and snippet to next node. |
| 2. Classify | HTTP Request → Anthropic API | Sends email content to Claude. Returns a JSON object with category, summary, and a draft reply. |
| 3. Parse | Code node | Extracts Claude’s JSON response. Handles markdown fences in case Claude wraps the output. |
| 4. Route | Switch node | Routes to one of 5 branches based on the category Claude returned. |
| 5a. Urgent | Gmail + Telegram | Labels IMPORTANT, saves draft reply, fires Telegram alert 🚨 |
| 5b. Sales Lead | Gmail + Telegram | Labels IMPORTANT, saves draft reply, fires Telegram alert 💰 |
| 5c. Support | Gmail | Labels as Personal, saves draft reply. No alert — review when ready. |
| 5d. Newsletter | Gmail | Labels as Promotions. No draft, no alert. |
| 5e. Spam | Gmail | Marked 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
- Add a Gmail Trigger node to your canvas
- Set Authentication to OAuth2 and connect your Gmail credential
- Set Event to Message Received
- Under Filters, set Read Status to Unread only
- 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 addressSubject— the email subject linesnippet— a short preview of the email body (lowercase, the exception)id— the Gmail message IDthreadId— 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.

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
- Add an HTTP Request node
- Set Method to POST
- Set URL to
https://api.anthropic.com/v1/messages - Set Authentication to Predefined Credential Type → Anthropic and connect your Anthropic API credential
- Add these Headers:
anthropic-version:2023-06-01content-type:application/json
- 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.

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.

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
- Add a Switch node and set Mode to Rules
- Add five rules, one per category
- For each rule: Left Value =
{{ $json.category }}, Operator = equals, Right Value = the category name (e.g.Urgent) - 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.

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.

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
- Open Telegram and search for @BotFather
- Send
/newbot - Give it a display name — e.g. Alex Harte Alerts
- Give it a username — must end in
bot, e.g. alexharte_alerts_bot - 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:
- Go to your bot in Telegram and send it any message (just type
hi) - Paste this URL into your browser, replacing
YOUR_BOT_TOKENwith the full token BotFather gave you:https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates - 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
- In n8n → Credentials → Add → search Telegram → paste the bot token → Save
- Assign the credential to both Telegram nodes in the workflow
- Paste your Chat ID into the Chat ID field on both nodes
- 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_replycorrectly — check the Code node output in the execution panel - You’re referencing
$json.draft_replybut the Label node upstream has overwritten$jsonwith 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?
| Component | Cost |
|---|---|
| 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 bot | Free |
| Gmail API | Free |
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:
- System 01 — MailerLite Lead Magnet Automation — always-on lead capture and nurture, built natively in MailerLite
- System 02 — Content Repurposing Machine — Build it here
- System 03 — AI Content Pipeline — Build it here
- System 04 — AI Brand Asset Generator — Build it here
Or download the full Automation Playbook PDF to get all five systems in one place.
Alex Harte | alexanderharte.com
