This workflow follows the Gmail Trigger → Google Sheets 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 →
{
"id": "Pl7ZdR3PNRcgfzMF",
"name": "[0] Gmail Trigger \u2192 Classify \u2192 Route to Pipeline (FIXED)",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"simple": false,
"filters": {
"readStatus": "unread"
},
"options": {}
},
"id": "a1dc6e81-0362-471a-b2dd-8d2ca9c9de83",
"name": "Watch for Emails",
"type": "n8n-nodes-base.gmailTrigger",
"typeVersion": 1,
"position": [
-656,
172
],
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"notes": "CRITICAL SETTINGS:\n1. Simplify = OFF (needed for threadId and labelIds)\n2. Poll every 5 minutes (adjust based on responsiveness needs)\n3. Read Status = Unread only\n\nThis triggers on new unread emails and passes full metadata."
},
{
"parameters": {
"jsCode": "// Email Pre-Filter v1.2 - FIXED to work with Extract Full Email Body node\n// 4-layer filtering: Domain, Headers, Patterns, Gmail Categories\n// Catches 60-80% of non-customer emails BEFORE AI classification\n\nconst items = $input.all();\nconst results = [];\n\n// ============ CONFIGURATION ============\nconst COMPANY_DOMAINS = [\n 'hattieb.com',\n 'hattiebshotchicken.com',\n 'aibuildlab.com'\n];\n\nconst AUTO_REPLY_SENDERS = [\n /noreply@/i,\n /no-reply@/i,\n /notifications?@/i,\n /alerts?@/i,\n /mailer-daemon@/i,\n /postmaster@/i,\n /donotreply@/i\n];\n\nconst AUTO_REPLY_SUBJECTS = [\n /out of office/i,\n /automatic reply/i,\n /auto-reply/i,\n /auto reply/i,\n /vacation.*reply/i,\n /away from (my )?office/i,\n /delivery status notification/i,\n /undeliverable/i,\n /mail delivery failed/i\n];\n\nconst SKIP_GMAIL_CATEGORIES = [\n 'CATEGORY_PROMOTIONS',\n 'CATEGORY_SOCIAL',\n 'SPAM'\n];\n// =========================================\n\nfor (const item of items) {\n const email = item.json;\n \n // Extract email fields - handle BOTH new Extract node output AND old Gmail structure\n let from, subject, fullBody, emailId, threadId, labelIds, date;\n \n if (email.fullEmailBody) {\n // NEW structure from \"Extract Full Email Body\" node\n from = email.from || '';\n subject = email.subject || '';\n fullBody = email.fullEmailBody;\n emailId = email.id || email.messageId;\n threadId = email.threadId;\n labelIds = email.labelIds || [];\n date = email.date;\n } else {\n // OLD structure from Gmail Trigger (fallback)\n from = email.from?.text || email.from?.value?.[0]?.address || email.From || '';\n subject = email.subject || email.Subject || '';\n fullBody = email.text || email.textPlain || email.body || email.snippet || '';\n emailId = email.id || email.messageId;\n threadId = email.threadId;\n labelIds = email.labelIds || [];\n date = email.internalDate || email.date;\n }\n \n const fromLower = from.toLowerCase();\n \n // Parse headers\n let headers = {};\n if (email.payload && email.payload.headers) {\n email.payload.headers.forEach(h => {\n headers[h.name.toLowerCase()] = h.value;\n });\n } else if (email.headers) {\n headers = email.headers;\n }\n \n // ============ LAYER 1: OWN DOMAIN (Loop Prevention) ============\n for (const domain of COMPANY_DOMAINS) {\n if (fromLower.includes('@' + domain.toLowerCase())) {\n results.push({\n json: {\n skip: true,\n reason: 'own_domain',\n classification_override: 'internal',\n confidence: 1.0,\n message: `Email from own domain (${domain}) - loop prevention`,\n email_id: emailId,\n thread_id: threadId,\n from: from,\n subject: subject\n }\n });\n continue;\n }\n }\n if (results.length > 0 && results[results.length - 1].json.skip) continue;\n \n // ============ LAYER 2: AUTO-REPLY HEADERS ============\n const autoReplyHeaders = [\n 'x-auto-response-suppress',\n 'auto-submitted',\n 'x-autoreply',\n 'x-autorespond'\n ];\n \n for (const headerName of autoReplyHeaders) {\n const headerValue = headers[headerName];\n if (headerValue && headerValue.toLowerCase() !== 'no') {\n results.push({\n json: {\n skip: true,\n reason: 'auto_reply_header',\n classification_override: 'automated',\n confidence: 1.0,\n message: `Auto-reply header detected: ${headerName}`,\n email_id: emailId,\n thread_id: threadId,\n from: from,\n subject: subject\n }\n });\n break;\n }\n }\n if (results.length > 0 && results[results.length - 1].json.skip) continue;\n \n // Check Precedence header\n const precedence = headers['precedence'];\n if (precedence && /bulk|junk|list/i.test(precedence)) {\n results.push({\n json: {\n skip: true,\n reason: 'bulk_mail',\n classification_override: 'automated',\n confidence: 0.95,\n message: `Bulk mail header: Precedence: ${precedence}`,\n email_id: emailId,\n thread_id: threadId,\n from: from,\n subject: subject\n }\n });\n continue;\n }\n \n // ============ LAYER 3: PATTERN MATCHING ============\n for (const pattern of AUTO_REPLY_SENDERS) {\n if (pattern.test(fromLower)) {\n results.push({\n json: {\n skip: true,\n reason: 'auto_reply_sender',\n classification_override: 'automated',\n confidence: 0.95,\n message: `Known auto-reply sender pattern: ${from}`,\n email_id: emailId,\n thread_id: threadId,\n from: from,\n subject: subject\n }\n });\n break;\n }\n }\n if (results.length > 0 && results[results.length - 1].json.skip) continue;\n \n for (const pattern of AUTO_REPLY_SUBJECTS) {\n if (pattern.test(subject)) {\n results.push({\n json: {\n skip: true,\n reason: 'auto_reply_subject',\n classification_override: 'automated',\n confidence: 0.90,\n message: `Subject matches auto-reply pattern`,\n email_id: emailId,\n thread_id: threadId,\n from: from,\n subject: subject\n }\n });\n break;\n }\n }\n if (results.length > 0 && results[results.length - 1].json.skip) continue;\n \n // ============ LAYER 4: GMAIL CATEGORIES ============\n for (const category of SKIP_GMAIL_CATEGORIES) {\n if (labelIds.includes(category)) {\n results.push({\n json: {\n skip: true,\n reason: 'gmail_category',\n classification_override: category === 'SPAM' ? 'spam' : 'automated',\n confidence: 0.90,\n message: `Gmail category: ${category}`,\n email_id: emailId,\n thread_id: threadId,\n from: from,\n subject: subject\n }\n });\n break;\n }\n }\n if (results.length > 0 && results[results.length - 1].json.skip) continue;\n \n // ============ PASSED ALL FILTERS ============\n results.push({\n json: {\n skip: false,\n reason: null,\n email_data: {\n id: emailId,\n thread_id: threadId,\n from: from,\n subject: subject,\n snippet: fullBody.substring(0, 500),\n body: fullBody,\n labelIds: labelIds,\n date: date\n }\n }\n });\n}\n\nreturn results;"
},
"id": "a803feb2-73da-4bd1-9125-231f1e243201",
"name": "Free Filter (Pre-Check)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
16,
172
],
"notes": "4-layer pre-filter catches 60-80% of emails for FREE:\n\n1. Own Domain - Prevents infinite reply loops (CRITICAL)\n2. Auto-Reply Headers - X-Auto-Response-Suppress, etc.\n3. Pattern Matching - noreply@, Out of Office, etc.\n4. Gmail Categories - Promotions, Social, Spam\n\nv1.1: FIXED email body extraction to get full content, not truncated snippet\n\nEDIT THE COMPANY_DOMAINS ARRAY with your domain(s)."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "condition-skip",
"leftValue": "={{ $json.skip }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "a4ff004d-dde7-4d25-a9a3-048b32a097f3",
"name": "Pre-Filtered?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
240,
172
],
"notes": "Routes emails based on pre-filter result:\n- TRUE (skip=true): Email was filtered, log and end\n- FALSE (skip=false): Continue to AI classification"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"mode": "id",
"value": "1xTng5h4vCWzFRtEvs05FX1lwguB6Mob-2G_LTgicn4I"
},
"sheetName": {
"__rl": true,
"value": 2063146029,
"mode": "list",
"cachedResultName": "Pre-Filter Log",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1xTng5h4vCWzFRtEvs05FX1lwguB6Mob-2G_LTgicn4I/edit#gid=2063146029"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"timestamp": "={{ $now.toISO() }}",
"email_id": "={{ $json.email_id }}",
"from": "={{ $json.from }}",
"subject": "={{ $json.subject }}",
"reason": "={{ $json.reason }}",
"classification": "={{ $json.classification_override }}",
"confidence": "={{ $json.confidence }}",
"message": "={{ $json.message }}"
},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "a51aed4b-c39e-4d0b-a070-61da6e170144",
"name": "Log Skipped Email",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
528,
0
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"notes": "Logs pre-filtered emails to Google Sheets."
},
{
"parameters": {
"inputText": "={{ 'Subject: ' + $json.email_data.subject + '\\n\\nFrom: ' + $json.email_data.from + '\\n\\nBody: ' + ($json.email_data.snippet || $json.email_data.body).substring(0, 1000) }}",
"categories": {
"categories": [
{
"category": "customer_service",
"description": "A real customer or potential customer needing help, information, or support. Includes: product questions, order issues, account problems, complaints, feature requests, or general inquiries. WHEN IN DOUBT, USE THIS CATEGORY."
},
{
"category": "spam",
"description": "Promotional content, marketing emails, unsolicited offers, or junk mail."
},
{
"category": "internal",
"description": "Communications from within the company or related to internal operations."
},
{
"category": "automated",
"description": "System-generated emails that don't require human response."
},
{
"category": "other",
"description": "Content that doesn't clearly fit the above categories. Use sparingly."
}
]
},
"options": {}
},
"id": "a7e1117d-772c-4747-a36c-84f227c2fcc2",
"name": "AI Classification",
"type": "@n8n/n8n-nodes-langchain.textClassifier",
"typeVersion": 1,
"position": [
464,
344
],
"notes": "Classifies email into 5 categories using AI."
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "class-result",
"name": "classification",
"value": "customer_service",
"type": "string"
},
{
"id": "class-confidence",
"name": "confidence",
"value": 0.9,
"type": "number"
},
{
"id": "original-email",
"name": "email_data",
"value": "={{ $('Free Filter (Pre-Check)').item.json.email_data }}",
"type": "object"
}
]
},
"options": {}
},
"id": "a0c81aa2-5715-4df7-8b13-05ffcc478cca",
"name": "Format Classification",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
816,
344
],
"notes": "Formats classification result for logging and routing."
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"mode": "id",
"value": "1xTng5h4vCWzFRtEvs05FX1lwguB6Mob-2G_LTgicn4I"
},
"sheetName": {
"__rl": true,
"value": 871095556,
"mode": "list",
"cachedResultName": "Classification Log"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"timestamp": "={{ $now.toISO() }}",
"email_id": "={{ $json.email_data.id }}",
"thread_id": "={{ $json.email_data.thread_id }}",
"from": "={{ $json.email_data.from }}",
"subject": "={{ $json.email_data.subject }}",
"classification": "={{ $json.classification }}",
"confidence": "={{ $json.confidence }}",
"model": "gemini-2.5-flash",
"pre_filter": "passed"
},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "a406b8ae-6fe2-4aaf-a854-0c1773285ded",
"name": "Log Classification",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1040,
344
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"notes": "Logs AI classification results to Google Sheets."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "condition-customer",
"leftValue": "={{ $json.classification }}",
"rightValue": "customer_service",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "a86bb039-3e2c-4783-badd-eeb33a2f6528",
"name": "Customer Email?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1264,
344
],
"notes": "Routes based on classification."
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "output-status",
"name": "status",
"value": "CUSTOMER_EMAIL",
"type": "string"
},
{
"id": "output-next",
"name": "next_step",
"value": "CinnaMon sentiment analysis",
"type": "string"
},
{
"id": "output-email",
"name": "email_data",
"value": "={{ $('Format Classification').item.json.email_data }}",
"type": "object"
},
{
"id": "output-classification",
"name": "filter_result",
"value": "={{ { classification: $json.classification, confidence: $json.confidence } }}",
"type": "object"
}
]
},
"options": {}
},
"id": "a87687e0-7f44-4271-952b-9972f343bf0f",
"name": "Continue to CinnaMon",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1488,
248
],
"notes": "Customer email identified. Output ready for CinnaMon agent."
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "archive-status",
"name": "status",
"value": "FILTERED",
"type": "string"
},
{
"id": "archive-reason",
"name": "reason",
"value": "={{ 'Classified as: ' + $json.classification }}",
"type": "string"
},
{
"id": "archive-email",
"name": "email_id",
"value": "={{ $json.email_data.id }}",
"type": "string"
}
]
},
"options": {}
},
"id": "a911c8c4-d6c8-4597-b26a-69cafc6a551b",
"name": "End (Not Customer)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1488,
440
],
"notes": "Non-customer email. Workflow ends here."
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "C2KIpNrNjr5TPMq6",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"email_id": "={{ $json.email_data.id }}",
"thread_id": "={{ $json.email_data.thread_id }}",
"from": "={{ $json.email_data.from }}",
"subject": "={{ $json.email_data.subject }}",
"body": "={{ $json.email_data.body }}",
"timestamp": "={{ $json.email_data.date }}",
"classification": "={{ $json.filter_result.classification }}"
},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "aexecute-w1-pipeline",
"name": "Execute W1 Pipeline",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.3,
"position": [
1712,
248
],
"notes": "Triggers W1 Email Processing Pipeline with customer email data."
},
{
"parameters": {
"options": {
"temperature": 0.1
}
},
"id": "a36cd1b0-d187-46c4-a82e-b4b76c46626a",
"name": "Gemini Flash 2.5",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"typeVersion": 1,
"position": [
536,
664
],
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"notes": "Gemini Flash 2.5-Lite for cost-effective classification."
},
{
"parameters": {
"url": "=https://gmail.googleapis.com/gmail/v1/users/me/messages/{{ $json.id }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "gmailOAuth2",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "format",
"value": "full"
}
]
},
"options": {}
},
"id": "aget-full-email-http",
"name": "Get Full Email via API",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
-432,
172
],
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Extract and decode full email body from Gmail API response\nconst message = $input.first().json;\n\nfunction extractEmailBody(message) {\n const id = message?.id || '';\n const threadId = message?.threadId || '';\n const labelIds = message?.labelIds || [];\n \n if (!message || !message.payload) {\n return { \n id,\n threadId,\n labelIds,\n fullEmailBody: message?.snippet || \"No email content found\",\n subject: '',\n from: '',\n date: ''\n };\n }\n \n function findPlainText(parts) {\n for (const part of parts) {\n if (part.mimeType === 'text/plain' && part.body?.data) {\n return part.body.data;\n }\n if (part.parts) {\n const found = findPlainText(part.parts);\n if (found) return found;\n }\n }\n return null;\n }\n \n let encodedBody = null;\n \n if (message.payload.body?.data) {\n encodedBody = message.payload.body.data;\n } else if (message.payload.parts) {\n encodedBody = findPlainText(message.payload.parts);\n }\n \n const headers = message.payload.headers || [];\n const subject = headers.find(h => h.name === 'Subject')?.value || '';\n const from = headers.find(h => h.name === 'From')?.value || '';\n const date = headers.find(h => h.name === 'Date')?.value || '';\n \n if (!encodedBody) {\n return {\n id,\n threadId,\n labelIds,\n fullEmailBody: message.snippet || \"Could not extract email body\",\n subject,\n from,\n date\n };\n }\n \n const base64url = encodedBody.replace(/-/g, '+').replace(/_/g, '/');\n const padding = 4 - (base64url.length % 4);\n const base64 = padding !== 4 ? base64url + '='.repeat(padding) : base64url;\n \n try {\n const decoded = Buffer.from(base64, 'base64').toString('utf8');\n return { id, threadId, labelIds, fullEmailBody: decoded, subject, from, date };\n } catch (error) {\n return { \n id,\n threadId,\n labelIds,\n fullEmailBody: `Error decoding: ${error.message}`,\n subject,\n from,\n date\n };\n }\n}\n\nreturn extractEmailBody(message);"
},
"id": "aextract-email-body",
"name": "Extract Full Email Body",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-208,
172
]
}
],
"connections": {
"Watch for Emails": {
"main": [
[
{
"node": "Get Full Email via API",
"type": "main",
"index": 0
}
]
]
},
"Free Filter (Pre-Check)": {
"main": [
[
{
"node": "Pre-Filtered?",
"type": "main",
"index": 0
}
]
]
},
"Pre-Filtered?": {
"main": [
[
{
"node": "Log Skipped Email",
"type": "main",
"index": 0
}
],
[
{
"node": "AI Classification",
"type": "main",
"index": 0
}
]
]
},
"AI Classification": {
"main": [
[
{
"node": "Format Classification",
"type": "main",
"index": 0
}
]
]
},
"Format Classification": {
"main": [
[
{
"node": "Log Classification",
"type": "main",
"index": 0
}
]
]
},
"Log Classification": {
"main": [
[
{
"node": "Customer Email?",
"type": "main",
"index": 0
}
]
]
},
"Customer Email?": {
"main": [
[
{
"node": "Continue to CinnaMon",
"type": "main",
"index": 0
}
],
[
{
"node": "End (Not Customer)",
"type": "main",
"index": 0
}
]
]
},
"Continue to CinnaMon": {
"main": [
[
{
"node": "Execute W1 Pipeline",
"type": "main",
"index": 0
}
]
]
},
"Gemini Flash 2.5": {
"ai_languageModel": [
[
{
"node": "AI Classification",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Get Full Email via API": {
"main": [
[
{
"node": "Extract Full Email Body",
"type": "main",
"index": 0
}
]
]
},
"Extract Full Email Body": {
"main": [
[
{
"node": "Free Filter (Pre-Check)",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false
},
"meta": null
}
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.
gmailOAuth2googlePalmApigoogleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
[0] Gmail Trigger → Classify → Route to Pipeline (FIXED). Uses gmailTrigger, googleSheets, textClassifier, lmChatGoogleGemini. Event-driven trigger; 14 nodes.
Source: https://github.com/8Dvibes/mindvalley-ai-mastery-students/blob/main/workflows/00-Filter-Email-Classification.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.
Email Classification Filter v1. Uses gmailTrigger, googleSheets, textClassifier, lmChatGoogleGemini. Event-driven trigger; 11 nodes.
This n8n template automatically classifies incoming emails (Sales, Support, Internal, Finance, Promotions) and routes them to a dedicated OpenAI LLM Agent for processing. Depending on the category, th
This n8n template automates scraping content from Skool communities using the Olostep API. It collects structured data from Skool pages and stores it in a clean format, making it easy to analyze commu
It is ideal for businesses handling vendor invoices, reimbursement forms, or bulk document intake.
This workflow is perfect for IT departments, helpdesk teams, or internal service units that manage incoming support requests through Jotform. It automates ticket handling, classification, and response