This workflow follows the Agent → Datatable recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Email Triage \u2014 Parse / Classify / Route / Auto-respond",
"active": false,
"nodes": [
{
"id": "n_trigger",
"name": "Gmail Trigger",
"type": "n8n-nodes-base.gmailTrigger",
"typeVersion": 1.3,
"position": [
0,
304
],
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"simple": false,
"filters": {
"q": "-from:me -label:n8n-processed"
},
"options": {}
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"id": "n_extract",
"name": "Extract & Loop Guard",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
304
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const BOT_ADDRESS = '<YOUR-BOT-ADDRESS>';\nconst j = $input.item.json;\nlet h = {};\nif (j.headers && typeof j.headers === 'object' && !Array.isArray(j.headers)) {\n for (const k of Object.keys(j.headers)) h[k.toLowerCase()] = j.headers[k];\n} else if (j.payload && Array.isArray(j.payload.headers)) {\n for (const x of j.payload.headers) h[x.name.toLowerCase()] = x.value;\n}\nconst fromHeader = h['from'] || j.from || '';\nconst fromAddr = ((fromHeader.match(/<([^>]+)>/) || [, fromHeader])[1] || '').toLowerCase().trim();\nconst fromName = fromHeader.replace(/<[^>]+>/, '').trim().replace(/^\"|\"$/g, '');\nconst subject = h['subject'] || j.subject || '';\nlet body = j.text || j.snippet || '';\nif (!body && j.payload && j.payload.body && j.payload.body.data) {\n try { body = Buffer.from(j.payload.body.data, 'base64').toString('utf-8'); } catch(e) {}\n}\nif (!body && j.payload && Array.isArray(j.payload.parts)) {\n for (const part of j.payload.parts) {\n if (part.mimeType === 'text/plain' && part.body && part.body.data) {\n try { body = Buffer.from(part.body.data, 'base64').toString('utf-8'); break; } catch(e) {}\n }\n }\n}\nconst autoSub = (h['auto-submitted'] || '').toLowerCase();\nconst isAutoSub = !!autoSub && autoSub !== 'no';\nconst precedence = (h['precedence'] || '').toLowerCase();\nconst isBulk = ['bulk', 'auto_reply', 'list'].includes(precedence);\nconst isDaemon = /^(mailer-daemon|postmaster|noreply|no-reply|donotreply|do-not-reply|bounce|bounces)@/i.test(fromAddr);\nconst isSelfLoop = !!fromAddr && fromAddr === BOT_ADDRESS.toLowerCase();\nconst hasListUnsub = !!h['list-unsubscribe'];\nconst loopGuard = isAutoSub || isBulk || isDaemon || isSelfLoop || hasListUnsub;\nconst loopGuardReason = isAutoSub ? 'auto-submitted' : isBulk ? 'bulk-precedence' : isDaemon ? 'daemon-sender' : isSelfLoop ? 'self-loop' : hasListUnsub ? 'list-unsubscribe' : '';\nreturn { json: {\n messageId: j.id,\n threadId: j.threadId,\n fromAddress: fromAddr,\n fromName,\n firstName: (fromName.split(/[\\s,]+/)[0]) || 'there',\n subject,\n bodyForAI: body.slice(0, 4000),\n bodyShort: body.slice(0, 200),\n loopGuard,\n loopGuardReason,\n receivedAt: new Date().toISOString()\n} };"
}
},
{
"id": "n_dedup",
"name": "Dedup (skip if seen)",
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
480,
304
],
"parameters": {
"operation": "rowNotExists",
"dataTableId": {
"__rl": true,
"mode": "id",
"value": "<YOUR-EMAIL-LOG-DATATABLE-ID>"
},
"filters": {
"conditions": [
{
"keyName": "messageId",
"keyValue": "={{ $json.messageId }}"
}
]
}
}
},
{
"id": "n_chat",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.3,
"position": [
720,
528
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4o-mini"
},
"builtInTools": {},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"id": "n_parser",
"name": "Classification Schema",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"typeVersion": 1.3,
"position": [
928,
528
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"required\": [\"category\", \"priority\", \"confidence\", \"action_items\"],\n \"properties\": {\n \"category\": { \"type\": \"string\", \"enum\": [\"support\", \"sales\", \"billing\", \"spam\"] },\n \"priority\": { \"type\": \"string\", \"enum\": [\"low\", \"normal\", \"high\", \"urgent\"] },\n \"confidence\": { \"type\": \"number\", \"minimum\": 0, \"maximum\": 1 },\n \"action_items\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"maxItems\": 3 }\n }\n}"
}
},
{
"id": "n_agent",
"name": "AI Email Classifier",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
848,
304
],
"parameters": {
"promptType": "define",
"text": "=Classify this email and extract structured data as JSON.\n\n--- EMAIL METADATA ---\nFrom: {{ $json.fromAddress }}\nSubject: {{ $json.subject }}\n\n--- EMAIL BODY (first 4000 chars) ---\n{{ $json.bodyForAI }}",
"hasOutputParser": true,
"options": {
"systemMessage": "You are an email triage classifier for a B2B SaaS company.\n\nCategories:\n- support: bug reports, error messages, feature requests, technical questions, product issues, account help\n- sales: pricing inquiries, demo requests, evaluation, prospect outreach, contract questions, partnership offers\n- billing: invoices, refunds, payment failures, subscription changes, payment confirmations, finance questions\n- spam: promotional content, irrelevant cold outreach, phishing, newsletters not asked for\n\nPriority: low | normal | high | urgent. Reserve 'urgent' for production outages, security issues, or angry escalations.\n\nConfidence: 0.0\u20131.0. Be conservative \u2014 if the email is ambiguous, set confidence below 0.7 so it routes to human review.\n\nReturn JSON matching the provided schema. action_items lists at most 3 concrete next steps for the human handler."
}
}
},
{
"id": "n_normalize",
"name": "Normalize & Decide",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1088,
304
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const CONFIDENCE_THRESHOLD = 0.7;\nconst j = $input.item.json;\nlet parsed = j.output != null ? j.output : j;\nif (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch(e) { parsed = {}; } }\nconst category = ((parsed && parsed.category) || 'spam').toLowerCase();\nconst confidence = (parsed && typeof parsed.confidence === 'number') ? parsed.confidence : 0;\nconst priority = (parsed && parsed.priority) || 'normal';\nconst action_items = (parsed && Array.isArray(parsed.action_items)) ? parsed.action_items : [];\nconst meta = $('Extract & Loop Guard').item.json;\nlet route_index = 3;\nif (confidence >= CONFIDENCE_THRESHOLD) {\n if (category === 'support') route_index = 0;\n else if (category === 'sales') route_index = 1;\n else if (category === 'billing') route_index = 2;\n}\nconst should_reply = !meta.loopGuard\n && confidence >= CONFIDENCE_THRESHOLD\n && ['support', 'sales', 'billing'].includes(category);\nlet routed_to = 'quarantine';\nif (route_index === 0) routed_to = 'zendesk';\nelse if (route_index === 1) routed_to = 'hubspot';\nelse if (route_index === 2) routed_to = 'finance-forward';\nreturn { json: {\n messageId: meta.messageId,\n threadId: meta.threadId,\n fromAddress: meta.fromAddress,\n fromName: meta.fromName,\n firstName: meta.firstName,\n subject: meta.subject,\n bodyShort: meta.bodyShort,\n loopGuard: meta.loopGuard,\n loopGuardReason: meta.loopGuardReason,\n category,\n confidence,\n priority,\n action_items,\n route_index,\n routed_to,\n should_reply,\n classified_at: new Date().toISOString()\n} };"
}
},
{
"id": "n_switch",
"name": "Route by Category",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
1328,
304
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.route_index }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "support"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.route_index }}",
"rightValue": 1,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "sales"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.route_index }}",
"rightValue": 2,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "billing"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.route_index }}",
"rightValue": 3,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "quarantine"
}
]
},
"options": {
"fallbackOutput": "none"
}
}
},
{
"id": "n_zendesk",
"name": "Zendesk Create Ticket",
"type": "n8n-nodes-base.zendesk",
"typeVersion": 1,
"position": [
1568,
80
],
"onError": "continueRegularOutput",
"parameters": {
"description": "=From: {{ $json.fromName }} <{{ $json.fromAddress }}>\nPriority: {{ $json.priority }}\nConfidence: {{ $json.confidence }}\n\nAction items the AI flagged:\n{{ ($json.action_items || []).map(a => '- ' + a).join('\\n') }}\n\n--- Original message snippet ---\n{{ $json.bodyShort }}",
"additionalFields": {
"tags": [
"email-triage",
"ai-classified",
"={{ $json.priority }}"
]
}
}
},
{
"id": "n_slack_support",
"name": "Slack #support",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.4,
"position": [
1808,
80
],
"onError": "continueRegularOutput",
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"mode": "name",
"value": "support"
},
"text": "=:incoming_envelope: New *support* email from `{{ $json.fromAddress }}` \u2014 _{{ $json.subject }}_ (priority: {{ $json.priority }}, confidence: {{ $json.confidence }})",
"otherOptions": {}
}
},
{
"id": "n_hubspot",
"name": "HubSpot Upsert Contact",
"type": "n8n-nodes-base.hubspot",
"typeVersion": 2.2,
"position": [
1568,
240
],
"onError": "continueRegularOutput",
"parameters": {
"email": "={{ $json.fromAddress }}",
"additionalFields": {
"firstName": {
"__rl": true,
"value": "={{ $json.firstName }}",
"mode": "expression"
},
"leadStatus": "NEW"
},
"options": {}
}
},
{
"id": "n_slack_sales",
"name": "Slack #sales",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.4,
"position": [
1808,
240
],
"onError": "continueRegularOutput",
"parameters": {
"authentication": "oAuth2",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "name",
"value": "sales"
},
"text": "=:moneybag: New *sales* lead from `{{ $json.fromAddress }}` \u2014 _{{ $json.subject }}_ (priority: {{ $json.priority }}, confidence: {{ $json.confidence }})",
"otherOptions": {}
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"id": "n_gmail_forward",
"name": "Forward to Finance",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.2,
"position": [
1568,
400
],
"onError": "continueRegularOutput",
"parameters": {
"sendTo": "finance@example.com",
"subject": "=[FWD][BILLING] {{ $json.subject }}",
"emailType": "text",
"message": "=Forwarded by email triage automation.\n\nOriginal sender: {{ $json.fromName }} <{{ $json.fromAddress }}>\nClassified: billing | priority {{ $json.priority }} | confidence {{ $json.confidence }}\n\nAction items:\n{{ ($json.action_items || []).map(a => '- ' + a).join('\\n') }}\n\n--- Snippet ---\n{{ $json.bodyShort }}",
"options": {}
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"id": "n_slack_billing",
"name": "Slack #billing",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.4,
"position": [
1808,
400
],
"onError": "continueRegularOutput",
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"mode": "name",
"value": "billing"
},
"text": "=:receipt: New *billing* email from `{{ $json.fromAddress }}` forwarded to finance \u2014 _{{ $json.subject }}_",
"otherOptions": {}
}
},
{
"id": "n_gmail_quarantine",
"name": "Apply Quarantine Label",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.2,
"position": [
1568,
560
],
"onError": "continueRegularOutput",
"parameters": {
"operation": "addLabels",
"messageId": "={{ $json.messageId }}",
"labelIds": [
"TRASH"
]
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"id": "n_slack_review",
"name": "Slack #email-review",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.4,
"position": [
1808,
560
],
"onError": "continueRegularOutput",
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"mode": "name",
"value": "email-review"
},
"text": "=:warning: Quarantined or low-confidence email from `{{ $json.fromAddress }}` \u2014 _{{ $json.subject }}_ (category: {{ $json.category }}, confidence: {{ $json.confidence }}, loopGuard: {{ $json.loopGuard }} {{ $json.loopGuardReason }})",
"otherOptions": {}
}
},
{
"id": "n_log",
"name": "Log to email_log",
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
2048,
304
],
"onError": "continueRegularOutput",
"parameters": {
"dataTableId": {
"__rl": true,
"mode": "id",
"value": "<YOUR-EMAIL-LOG-DATATABLE-ID>"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"messageId": "={{ $json.messageId }}",
"threadId": "={{ $json.threadId }}",
"fromAddress": "={{ $json.fromAddress }}",
"subject": "={{ $json.subject }}",
"category": "={{ $json.category }}",
"confidence": "={{ $json.confidence }}",
"routed_to": "={{ $json.routed_to }}",
"replied": "={{ $json.should_reply }}",
"timestamp": "={{ $json.classified_at }}"
},
"matchingColumns": [
"messageId"
],
"schema": []
},
"options": {}
}
},
{
"id": "n_if_reply",
"name": "Should Auto-Reply?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2288,
304
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.should_reply }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"id": "condition-should-reply"
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "n_reply",
"name": "Gmail Reply In-Thread",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.2,
"position": [
2528,
208
],
"onError": "continueRegularOutput",
"parameters": {
"operation": "reply",
"messageId": "={{ $json.messageId }}",
"emailType": "text",
"message": "=Hi {{ $json.firstName }},\n\nThanks for reaching out \u2014 we received your message and our {{ $json.category }} team has it in their queue. Expected response time:\n\n \u2022 support: within 1 business day\n \u2022 sales: within 4 business hours\n \u2022 billing: within 2 business days\n\nIf this is urgent, reply with URGENT in the subject and we will escalate.\n\nThis acknowledgement was sent automatically.",
"options": {}
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Gmail Trigger": {
"main": [
[
{
"node": "Extract & Loop Guard",
"type": "main",
"index": 0
}
]
]
},
"Extract & Loop Guard": {
"main": [
[
{
"node": "Dedup (skip if seen)",
"type": "main",
"index": 0
}
]
]
},
"Dedup (skip if seen)": {
"main": [
[
{
"node": "AI Email Classifier",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Email Classifier",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Classification Schema": {
"ai_outputParser": [
[
{
"node": "AI Email Classifier",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"AI Email Classifier": {
"main": [
[
{
"node": "Normalize & Decide",
"type": "main",
"index": 0
}
]
]
},
"Normalize & Decide": {
"main": [
[
{
"node": "Route by Category",
"type": "main",
"index": 0
}
]
]
},
"Route by Category": {
"main": [
[
{
"node": "Zendesk Create Ticket",
"type": "main",
"index": 0
}
],
[
{
"node": "HubSpot Upsert Contact",
"type": "main",
"index": 0
}
],
[
{
"node": "Forward to Finance",
"type": "main",
"index": 0
}
],
[
{
"node": "Apply Quarantine Label",
"type": "main",
"index": 0
}
]
]
},
"Zendesk Create Ticket": {
"main": [
[
{
"node": "Slack #support",
"type": "main",
"index": 0
}
]
]
},
"HubSpot Upsert Contact": {
"main": [
[
{
"node": "Slack #sales",
"type": "main",
"index": 0
}
]
]
},
"Forward to Finance": {
"main": [
[
{
"node": "Slack #billing",
"type": "main",
"index": 0
}
]
]
},
"Apply Quarantine Label": {
"main": [
[
{
"node": "Slack #email-review",
"type": "main",
"index": 0
}
]
]
},
"Slack #support": {
"main": [
[
{
"node": "Log to email_log",
"type": "main",
"index": 0
}
]
]
},
"Slack #sales": {
"main": [
[
{
"node": "Log to email_log",
"type": "main",
"index": 0
}
]
]
},
"Slack #billing": {
"main": [
[
{
"node": "Log to email_log",
"type": "main",
"index": 0
}
]
]
},
"Slack #email-review": {
"main": [
[
{
"node": "Log to email_log",
"type": "main",
"index": 0
}
]
]
},
"Log to email_log": {
"main": [
[
{
"node": "Should Auto-Reply?",
"type": "main",
"index": 0
}
]
]
},
"Should Auto-Reply?": {
"main": [
[
{
"node": "Gmail Reply In-Thread",
"type": "main",
"index": 0
}
],
[]
]
}
},
"settings": {
"executionOrder": "v1",
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner",
"binaryMode": "separate"
},
"tags": []
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2openAiApislackOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Email Triage — Parse / Classify / Route / Auto-respond. Uses gmailTrigger, dataTable, lmChatOpenAi, outputParserStructured. Event-driven trigger; 19 nodes.
Source: https://github.com/HustleDanie/Email-Triage-Automation/blob/d6fccf67da0cf41eea018a3376376e3a70649bf7/workflows/email-triage.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
An AI-powered Gmail assistant built with n8n that automatically labels emails, learns from your decisions, and safely improves over time using human-in-the-loop training.
This workflow automates legal policy governance for legal teams, policy managers, and compliance officers, eliminating manual document review, approval classification, and multi-channel stakeholder di
Streamline customer support with a real-time, AI-powered answer engine that detects incoming support emails, classifies intent, identifies the customer’s GEO region, and generates a tailored reply rea
Monitors your AP inbox for incoming invoices, extracts structured data with AI, runs duplicate and vendor history checks against Supabase, then scores each invoice for fraud risk — routing suspicious
Gmail users report spending significant time manually sorting email, so this tool helps alleviate that burden. Gmail Trigger monitors unread emails every 2 minutes Once an email arrives, the content i