Module 06 · business · intermediate
Lesson 06 · Gmail Inbox Triage
Goal
Your first AI workflow: let an LLM read each incoming email and decide what it is, then act on that decision. The pattern (trigger → prepare text → AI extracts structured fields → normal nodes act on them) is behind most "AI automation" gigs.
Prerequisites
- Modules 01–05
- An OpenAI API key (platform.openai.com → API keys). Small models like
gpt-5-miniare cheap per email; check current pricing on openai.com/api/pricing. - Gmail credential (from Module 05) for the inbox part
Concepts
- Cluster nodes: AI nodes have sub-nodes. The Information Extractor (root) needs a Chat Model (sub-node) plugged into its Model input. Connections between them are of type
ai_languageModel, notmain. - Information Extractor: turns free text into fields you define (name, type, description). The descriptions are your prompt; be specific about allowed values.
- Never trust the model blindly:
Triage Resultforces unknown categories tootherand normalizesurgentto a real boolean. Downstream nodes can then rely on clean values. - Two triggers, one brain: the Gmail Trigger is for production, the Webhook is for testing prompts quickly with curl (same idea as Module 02).
- Gmail labels need IDs: the Add Labels operation takes label IDs like
Label_123456, not names.
Build it
Step 1: Triggers and input
- Gmail Trigger
New Email: poll every minute, filter Read Status: Unread, keep Simplify on. - Webhook
Triage API: POSTtriage, respond with a Respond to Webhook node → IFHas Content?({{ ($json.body.subject ?? '') + ($json.body.text ?? '') }}is not empty), false → 400. - Edit Fields
Prepare Email(fed by both):message_id={{ $json.id ?? '' }},from={{ $json.From ?? $json.body?.from ?? '' }},subject={{ $json.Subject ?? $json.body?.subject ?? '' }},text={{ ($json.snippet ?? $json.body?.text ?? '').slice(0, 3000) }}.
The ?? chains let one node handle both the Gmail shape (From, Subject, snippet) and the API shape (body.from, …).
Step 2: Classify with AI
- Information Extractor
Classify Email. Text:From: {{ $json.from }}/Subject: {{ $json.subject }}/ blank line /{{ $json.text }}. - Attributes:
category(string, required): "Exactly one of: sales (…), support (…), billing (…), newsletter (…), personal (…), other"urgent(boolean, required): "true only if the sender needs a response within 24 hours or something is broken"summary(string, required): "One sentence, max 20 words, saying what the sender wants"
- Options → System Prompt: "You triage a freelancer's inbox… Use only the allowed category values, in lowercase."
- Click the Model input → OpenAI Chat Model → create the OpenAI credential → model
gpt-5-mini.
Step 3: Clean the result
Edit Fields Triage Result: copy message_id, from, subject from $('Prepare Email'), then:
category={{ ['sales','support','billing','newsletter','personal'].includes($json.output.category) ? $json.output.category : 'other' }}urgent(Boolean) ={{ $json.output.urgent === true || $json.output.urgent === 'true' }}summary={{ $json.output.summary }}
Step 4: Respond or label
IF Called via API? ({{ $('Triage API').isExecuted }} is true):
- true → Respond to Webhook
{{ $json }} - false → Edit Fields
Label Map (edit me)(include other fields) with an Object fieldlabel_idsmapping each category to a Gmail label ID → GmailApply Label(Message → Add Label, Message ID{{ $json.message_id }}, Label IDs expression{{ [$json.label_ids[$json.category]] }}) → IFUrgent?→ No OperationNotify Me (Exercise 2).
Step 5: Find your label IDs
Create labels in Gmail (e.g. AI/Sales, AI/Support, …). Then in n8n add a temporary Gmail node → Label → Get Many, run it, copy each id into Label Map, and delete the temporary node.
Test it
./test/test.sh # validation only
WITH_CREDS=1 ./test/test.sh # + 4 classification cases (needs the OpenAI credential)
Then publish, send yourself a test email and watch it get labeled within a minute.
Common problems
- "Node does not have any credentials set" / "Error in sub-node OpenAI Chat Model": select your OpenAI credential on the OpenAI Chat Model node.
- Categories like "Sales Lead" or "SUPPORT": tighten the attribute description;
Triage Resultalready maps anything unexpected toother. - "Invalid label": you used a label name. Use the ID from Label → Get Many.
- Same email processed twice: the trigger only fetches unread mail. Mark as read after labeling (Exercise 1).
- Costs: each email is one small request. Keep
textcapped (.slice(0, 3000)) so long threads don't cost more.