This workflow corresponds to n8n.io template #13463 — we link there as the canonical source.
This workflow follows the Agent → Gmail 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": "WORKFLOW_ID_PLACEHOLDER",
"name": "Classify Gmail emails with AI and auto-label with smart reply drafts using Ollama",
"tags": [],
"nodes": [
{
"id": "e654e5c9-b2d2-4fda-b7c9-848a63fc2d93",
"name": "Sticky Note - Main Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-400,
-112
],
"parameters": {
"color": 4,
"width": 740,
"height": 1000,
"content": "## Classify Gmail emails with AI and auto-label with smart reply drafts\n\nAutomatically classify every incoming email using a local AI model (Ollama), apply Gmail labels, and generate smart reply drafts for important messages \u2014 zero paid APIs required.\n\n### How it works\n1. **Gmail Trigger** watches your inbox for new unread emails every 30 minutes.\n2. **Extract Email Data** parses the sender, subject, body, and metadata into clean fields.\n3. **AI Classifier** (Ollama, local) analyzes the email and classifies it into one of 6 categories: \ud83d\udd34 Urgent, \ud83d\udccb Action Required, \ud83d\udcac Follow-up, \ud83d\udcf0 Newsletter, \ud83e\udd16 Automated, or \ud83d\udeab Spam/Promotional.\n4. **Smart Router** sends each email down its dedicated path based on classification.\n5. **Gmail Labels** are automatically applied so your inbox is organized without lifting a finger.\n6. **Reply Drafts** are generated by AI for Urgent and Action Required emails \u2014 ready for you to review, edit, and send.\n7. **Summary Log** tracks every processed email in Google Sheets for analytics and review.\n\n### Setup steps\n1. **Gmail OAuth** \u2014 Connect your Google account with read, modify, and compose permissions.\n2. **Create Gmail Labels** \u2014 In Gmail, manually create these labels:\n - `\ud83d\udd34 Urgent`, `\ud83d\udccb Action Required`, `\ud83d\udcac Follow-up`, `\ud83d\udcf0 Newsletter`, `\ud83e\udd16 Automated`, `\ud83d\udeab Spam-Promo`\n3. **Get Label IDs** \u2014 Use Gmail API or n8n to find each label's ID. Update the `labelIds` in each Gmail Label node.\n4. **Ollama** \u2014 Ensure Ollama is running locally with your preferred model pulled (default: `mistral`). Change the model name in the Ollama Chat Model node if needed.\n5. **Google Sheets** (optional) \u2014 Connect your credential and set a spreadsheet ID for the logging node.\n6. **Test** \u2014 Send yourself test emails (urgent, newsletter, promotional) and run manually to verify accuracy.\n\n### Customization\n- Swap `mistral` for `llama3`, `gemma2`, or any Ollama model.\n- Add more categories by editing the AI prompt and adding Switch outputs.\n- Change the reply draft tone (formal, casual, friendly) in the AI prompt.\n- Add Slack/Telegram notifications for urgent emails.\n- Add auto-archive for newsletters and spam.\n- Decrease the polling interval for near-real-time processing.\n- Add sender whitelist/blacklist logic before the AI node."
},
"typeVersion": 1
},
{
"id": "9fb47ffc-8479-4013-80dd-ea78e55d6227",
"name": "Sticky Note - Email Intake",
"type": "n8n-nodes-base.stickyNote",
"position": [
160,
960
],
"parameters": {
"color": 6,
"width": 560,
"height": 260,
"content": "## \ud83d\udce5 Email Intake\nGmail trigger watches inbox for unread emails, then extracts and cleans email data"
},
"typeVersion": 1
},
{
"id": "6c54a558-173b-44b8-80f8-38825cb5d8c6",
"name": "Sticky Note - AI Classification",
"type": "n8n-nodes-base.stickyNote",
"position": [
768,
960
],
"parameters": {
"color": 6,
"width": 520,
"height": 308,
"content": "## \ud83e\udd16 AI Classification\nOllama analyzes email content and returns category, priority, and reply draft"
},
"typeVersion": 1
},
{
"id": "c3a16e2d-adc1-4e00-a31a-c409814efb02",
"name": "Sticky Note - Route and Label",
"type": "n8n-nodes-base.stickyNote",
"position": [
1328,
768
],
"parameters": {
"color": 6,
"width": 960,
"height": 1004,
"content": "## \ud83d\udd00 Route, Label & Draft\nSwitch by category, apply Gmail labels, create reply drafts for important emails"
},
"typeVersion": 1
},
{
"id": "59d64a60-80df-426e-a485-43872e4bead0",
"name": "Sticky Note - Logging",
"type": "n8n-nodes-base.stickyNote",
"position": [
2352,
960
],
"parameters": {
"color": 6,
"width": 400,
"height": 296,
"content": "## \ud83d\udcca Logging\nEvery processed email is logged to Google Sheets for analytics"
},
"typeVersion": 1
},
{
"id": "31261316-e9da-490d-9bfa-3f3d30364b22",
"name": "Sticky Note - Warning Labels",
"type": "n8n-nodes-base.stickyNote",
"position": [
1344,
1840
],
"parameters": {
"color": 3,
"width": 360,
"content": "## \u26a0\ufe0f Create Gmail Labels First\nYou must manually create these labels in Gmail before activating:\n`\ud83d\udd34 Urgent`, `\ud83d\udccb Action Required`, `\ud83d\udcac Follow-up`, `\ud83d\udcf0 Newsletter`, `\ud83e\udd16 Automated`, `\ud83d\udeab Spam-Promo`\nThen update the Label IDs in each labeling node."
},
"typeVersion": 1
},
{
"id": "b0f03be3-f1c2-4f44-8ec9-ee2fd8865420",
"name": "Sticky Note - Warning Ollama",
"type": "n8n-nodes-base.stickyNote",
"position": [
784,
1344
],
"parameters": {
"color": 3,
"width": 300,
"height": 140,
"content": "## \u26a0\ufe0f Check Ollama Model\nDefault model: `mistral`. Make sure it's pulled:\n`ollama pull mistral`\nOr change to `llama3`, `gemma2`, etc."
},
"typeVersion": 1
},
{
"id": "db9674d4-8f0e-4594-85c3-fd60cce764c6",
"name": "\ud83d\udce5 Gmail Trigger",
"type": "n8n-nodes-base.gmailTrigger",
"position": [
208,
1088
],
"parameters": {
"simple": false,
"filters": {
"readStatus": "unread"
},
"options": {},
"pollTimes": {
"item": [
{
"mode": "everyX",
"unit": "minutes",
"value": 30
}
]
}
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "950c6313-8ef5-4f82-a8b0-2918e868bbfe",
"name": "\ud83d\udd27 Extract Email Data",
"type": "n8n-nodes-base.code",
"position": [
448,
1088
],
"parameters": {
"jsCode": "// Extract and clean email data for AI processing\nconst items = $input.all();\nconst processedEmails = [];\n\nfor (const item of items) {\n const email = item.json;\n \n // ============================================\n // SAFELY EXTRACT HEADERS (handles all formats)\n // ============================================\n \n function safeString(value) {\n if (!value) return '';\n if (typeof value === 'string') return value;\n if (Array.isArray(value)) return value.map(v => safeString(v)).join(', ');\n if (typeof value === 'object') {\n // Gmail sometimes returns { name: \"John\", email: \"user@example.com\" }\n if (value.value) return safeString(value.value);\n if (value.text) return safeString(value.text);\n if (value.email && value.name) return `${value.name} <${value.email}>`;\n if (value.email) return value.email;\n if (value.address) return value.address;\n return JSON.stringify(value);\n }\n return String(value);\n }\n \n function getHeader(headers, name) {\n if (!headers || !Array.isArray(headers)) return '';\n const found = headers.find(h => \n h && h.name && h.name.toLowerCase() === name.toLowerCase()\n );\n return found ? safeString(found.value) : '';\n }\n \n // Try multiple Gmail node output formats\n let fromHeader = '';\n let subjectHeader = '';\n let dateHeader = '';\n let toHeader = '';\n let ccHeader = '';\n let replyToHeader = '';\n \n // Format 1: Gmail Trigger v1 (flat fields)\n if (email.from) {\n fromHeader = safeString(email.from);\n subjectHeader = safeString(email.subject || '');\n dateHeader = safeString(email.date || email.internalDate || '');\n toHeader = safeString(email.to || '');\n ccHeader = safeString(email.cc || '');\n replyToHeader = safeString(email.replyTo || '');\n }\n \n // Format 2: Gmail API raw (payload.headers array)\n if (email.payload && email.payload.headers) {\n fromHeader = fromHeader || getHeader(email.payload.headers, 'from');\n subjectHeader = subjectHeader || getHeader(email.payload.headers, 'subject');\n dateHeader = dateHeader || getHeader(email.payload.headers, 'date');\n toHeader = toHeader || getHeader(email.payload.headers, 'to');\n ccHeader = ccHeader || getHeader(email.payload.headers, 'cc');\n replyToHeader = replyToHeader || getHeader(email.payload.headers, 'reply-to');\n }\n \n // Format 3: Some Gmail nodes use headers directly\n if (email.headers) {\n const h = email.headers;\n fromHeader = fromHeader || safeString(h.from || h.From || '');\n subjectHeader = subjectHeader || safeString(h.subject || h.Subject || '');\n dateHeader = dateHeader || safeString(h.date || h.Date || '');\n toHeader = toHeader || safeString(h.to || h.To || '');\n ccHeader = ccHeader || safeString(h.cc || h.Cc || '');\n }\n \n // Format 4: labelIds at top level (Gmail Trigger v2)\n if (!fromHeader && email.labelIds) {\n fromHeader = safeString(email.from || '');\n subjectHeader = safeString(email.subject || '');\n }\n \n // ============================================\n // PARSE SENDER NAME AND EMAIL\n // ============================================\n let senderName = '';\n let senderEmail = '';\n let senderDomain = '';\n \n // Ensure fromHeader is definitely a string before regex\n fromHeader = String(fromHeader || '');\n subjectHeader = String(subjectHeader || '');\n \n try {\n // Try \"Name <email>\" format\n const senderMatch = fromHeader.match(/^\"?([^\"<]+)\"?\\s*<?([^>]*)>?$/);\n if (senderMatch && senderMatch[2]) {\n senderName = senderMatch[1].trim();\n senderEmail = senderMatch[2].trim();\n } else if (fromHeader.includes('@')) {\n // Just an email address\n senderEmail = fromHeader.trim();\n senderName = senderEmail.split('@')[0];\n } else {\n senderName = fromHeader;\n senderEmail = fromHeader;\n }\n \n senderDomain = senderEmail.includes('@') ? senderEmail.split('@')[1] : '';\n } catch (e) {\n senderName = fromHeader;\n senderEmail = fromHeader;\n senderDomain = '';\n }\n \n // ============================================\n // EXTRACT BODY TEXT\n // ============================================\n let bodyText = '';\n \n // Try direct text fields first (most Gmail trigger versions)\n if (email.text) {\n bodyText = safeString(email.text);\n } else if (email.textPlain) {\n bodyText = safeString(email.textPlain);\n } else if (email.snippet) {\n bodyText = safeString(email.snippet);\n } else if (email.textHtml) {\n bodyText = safeString(email.textHtml);\n }\n \n // Try payload body\n if (!bodyText && email.payload) {\n if (email.payload.body && email.payload.body.data) {\n try {\n bodyText = Buffer.from(email.payload.body.data, 'base64').toString('utf-8');\n } catch (e) {}\n }\n \n // Try multipart\n if (!bodyText && email.payload.parts) {\n for (const part of email.payload.parts) {\n if (part.mimeType === 'text/plain' && part.body && part.body.data) {\n try {\n bodyText = Buffer.from(part.body.data, 'base64').toString('utf-8');\n break;\n } catch (e) {}\n }\n }\n // Fallback to HTML part\n if (!bodyText) {\n for (const part of email.payload.parts) {\n if (part.mimeType === 'text/html' && part.body && part.body.data) {\n try {\n bodyText = Buffer.from(part.body.data, 'base64').toString('utf-8');\n break;\n } catch (e) {}\n }\n }\n }\n }\n }\n \n // Use snippet as last resort\n if (!bodyText && email.snippet) {\n bodyText = safeString(email.snippet);\n }\n \n // Clean body text\n bodyText = String(bodyText || '')\n .replace(/<[^>]+>/g, ' ') // Strip HTML\n .replace(/\\r\\n/g, '\\n') // Normalize newlines\n .replace(/\\n{3,}/g, '\\n\\n') // Max 2 newlines\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/\\s+/g, ' ') // Collapse whitespace\n .trim()\n .substring(0, 3000); // Limit for AI\n \n // ============================================\n // DETECT FLAGS\n // ============================================\n const hasAttachments = !!(\n (email.payload && email.payload.parts && email.payload.parts.some(p => p.filename && p.filename.length > 0)) ||\n email.attachments ||\n (Array.isArray(email.attachments) && email.attachments.length > 0)\n );\n \n const isReply = subjectHeader.toLowerCase().startsWith('re:');\n const isForward = subjectHeader.toLowerCase().startsWith('fwd:') || subjectHeader.toLowerCase().startsWith('fw:');\n \n // ============================================\n // BUILD OUTPUT\n // ============================================\n processedEmails.push({\n json: {\n // Gmail metadata\n gmailId: email.id || email.messageId || '',\n threadId: email.threadId || '',\n labelIds: email.labelIds || [],\n \n // Parsed fields\n from: fromHeader,\n senderName: senderName,\n senderEmail: senderEmail,\n senderDomain: senderDomain,\n to: toHeader,\n cc: ccHeader,\n replyTo: replyToHeader || senderEmail,\n subject: subjectHeader,\n date: dateHeader,\n \n // Content\n bodyText: bodyText,\n bodyPreview: bodyText.substring(0, 300),\n \n // Flags\n hasAttachments: hasAttachments,\n isReply: isReply,\n isForward: isForward,\n \n // Processing\n processedAt: new Date().toISOString()\n }\n });\n}\n\nif (processedEmails.length === 0) {\n return [{ json: { error: 'No emails to process', processedAt: new Date().toISOString() } }];\n}\n\nreturn processedEmails;"
},
"typeVersion": 2
},
{
"id": "ce560c12-b054-404e-be51-d210d1b677d1",
"name": "\ud83e\udd16 AI Email Classifier",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
800,
1088
],
"parameters": {
"text": "=Classify this email and generate a reply draft if needed.\n\n--- EMAIL DATA ---\nFrom: {{ $json.from }}\nTo: {{ $json.to }}\nCC: {{ $json.cc }}\nSubject: {{ $json.subject }}\nDate: {{ $json.date }}\nHas Attachments: {{ $json.hasAttachments }}\nIs Reply: {{ $json.isReply }}\nIs Forward: {{ $json.isForward }}\nSender Domain: {{ $json.senderDomain }}\n\n--- EMAIL BODY ---\n{{ $json.bodyText }}\n\n--- CLASSIFY AND RESPOND ---",
"options": {
"systemMessage": "You are an expert email triage assistant. Your job is to classify incoming emails and generate appropriate reply drafts for important ones.\n\n---\n\nEMAIL CATEGORIES (choose exactly one):\n\n1. urgent\n - Requires immediate attention or response within hours\n - Time-sensitive requests, deadlines today/tomorrow\n - Boss/client escalations, critical issues\n - Payment/billing problems that need immediate action\n - Meeting in the next few hours that needs confirmation\n - Keywords: ASAP, urgent, immediately, deadline, critical, emergency, end of day, EOD\n\n2. action_required\n - Needs a response or action but not time-critical\n - Meeting requests, document reviews, approval requests\n - Questions that need thoughtful answers\n - Project updates requiring input\n - Invitations needing RSVP\n - Collaboration requests\n\n3. follow_up\n - Informational but may need future action\n - Status updates, FYI emails\n - Shared documents for review later\n - Introductions and networking\n - Ongoing conversation threads\n - Thank you emails that might need acknowledgment\n\n4. newsletter\n - Subscribed content, digests, roundups\n - Blog post notifications\n - Industry news, product updates from services you use\n - Weekly/monthly summaries\n - Educational content, webinar invites\n - Has unsubscribe link\n\n5. automated\n - System-generated notifications\n - Order confirmations, shipping updates\n - Password resets, login alerts\n - Calendar notifications\n - CI/CD pipeline alerts\n - Monitoring alerts, server notifications\n - Sent from noreply@ or notification@ addresses\n\n6. spam_promo\n - Unsolicited marketing, cold outreach you did not request\n - Promotional offers, discount codes\n - \"You've been selected\" type emails\n - Link-heavy promotional content\n - Mass marketing campaigns\n - Emails from unknown senders with salesy language\n\n---\n\nCLASSIFICATION RULES:\n\n1. Look at the sender domain \u2014 noreply@, notification@, marketing@ are strong signals.\n2. Check the subject line for urgency keywords.\n3. Analyze the body for calls to action, deadlines, or questions directed at the recipient.\n4. If the email is a reply in a thread (is_reply = true), it's more likely action_required or follow_up.\n5. Forwarded emails often need action or are FYI.\n6. Short emails with direct questions = action_required or urgent.\n7. Long emails with links and images = newsletter or spam_promo.\n8. When in doubt between newsletter and spam_promo, check if the sender seems legitimate.\n9. When in doubt between action_required and follow_up, lean toward action_required.\n\n---\n\nREPLY DRAFT RULES:\n\n- Generate a reply draft ONLY for: urgent, action_required, and follow_up categories.\n- Do NOT generate replies for: newsletter, automated, spam_promo.\n- Reply tone: Professional but warm. Not robotic.\n- Reply length: 30-80 words. Short and actionable.\n- Start with context acknowledgment, then address the ask.\n- End with a clear next step or question.\n- Do NOT include subject line in the reply body.\n- Do NOT include greetings like \"Dear\" \u2014 use first name.\n- Sign off with just \"Best,\" or \"Thanks,\" (the user will add their name).\n\n---\n\nPRIORITY SCORING:\n- 1 = Low (newsletters, automated, spam)\n- 2 = Medium (follow-up, informational)\n- 3 = High (action required, needs response)\n- 4 = Critical (urgent, time-sensitive)\n\n---\n\nRESPOND WITH ONLY RAW JSON. No markdown fences. No explanation. Start with { end with }.\n\n{\n \"category\": \"urgent\" or \"action_required\" or \"follow_up\" or \"newsletter\" or \"automated\" or \"spam_promo\",\n \"priority\": 1 to 4,\n \"confidence\": 0.0 to 1.0,\n \"reason\": \"One sentence explaining why this classification was chosen\",\n \"summary\": \"2-3 sentence summary of what the email is about\",\n \"keyAction\": \"What the recipient needs to do (if anything)\" or \"No action needed\",\n \"replyDraft\": \"The suggested reply text\" or \"\",\n \"replySubject\": \"Re: original subject\" or \"\",\n \"suggestedLabel\": \"\ud83d\udd34 Urgent\" or \"\ud83d\udccb Action Required\" or \"\ud83d\udcac Follow-up\" or \"\ud83d\udcf0 Newsletter\" or \"\ud83e\udd16 Automated\" or \"\ud83d\udeab Spam-Promo\",\n \"shouldAutoArchive\": true or false,\n \"sentimentTone\": \"positive\" or \"neutral\" or \"negative\" or \"urgent\"\n}"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 2.1
},
{
"id": "abdbf241-4eb7-47b4-983d-23da8b06848f",
"name": "\ud83c\udfaf Extract Classification",
"type": "n8n-nodes-base.code",
"position": [
1088,
1088
],
"parameters": {
"jsCode": "// Extract AI classification from response\nconst emailData = $('\ud83d\udd27 Extract Email Data').item.json;\nconst rawAiOutput = $json;\n\nlet classData = {};\n\nfunction extractJSON(str) {\n if (typeof str !== 'string') return null;\n let cleaned = str.replace(/```json\\s*/gi, '').replace(/```\\s*/g, '').trim();\n try { return JSON.parse(cleaned); } catch (e) {\n const match = cleaned.match(/\\{[\\s\\S]*\"category\"\\s*:[\\s\\S]*\\}/);\n if (match) { try { return JSON.parse(match[0]); } catch (e2) { return null; } }\n return null;\n }\n}\n\nfunction findAIResponse(obj, depth = 0) {\n if (depth > 5 || !obj || typeof obj !== 'object') return null;\n if (obj.category && typeof obj.category === 'string') return obj;\n const props = ['output', 'text', 'message', 'content', 'response', 'result', 'data', 'json', 'kwargs', 'lc_kwargs'];\n for (const prop of props) {\n if (obj[prop] !== undefined) {\n if (typeof obj[prop] === 'string') { const p = extractJSON(obj[prop]); if (p && p.category) return p; }\n else if (typeof obj[prop] === 'object') { const f = findAIResponse(obj[prop], depth + 1); if (f) return f; }\n }\n }\n for (const key of Object.keys(obj)) {\n if (props.includes(key)) continue;\n const val = obj[key];\n if (typeof val === 'string' && val.includes('\"category\"')) { const p = extractJSON(val); if (p && p.category) return p; }\n else if (typeof val === 'object' && val !== null) { const f = findAIResponse(val, depth + 1); if (f) return f; }\n }\n return null;\n}\n\ntry {\n if (rawAiOutput && rawAiOutput.category) { classData = rawAiOutput; }\n else { const found = findAIResponse(rawAiOutput); if (found) classData = found; }\n if (!classData.category) { const s = JSON.stringify(rawAiOutput); const p = extractJSON(s); if (p && p.category) classData = p; }\n} catch (error) {}\n\nconst validCategories = ['urgent', 'action_required', 'follow_up', 'newsletter', 'automated', 'spam_promo'];\n\nif (!classData.category || !validCategories.includes(classData.category)) {\n classData = {\n category: 'follow_up',\n priority: 2,\n confidence: 0.5,\n reason: 'Could not parse AI response \u2014 defaulting to follow_up',\n summary: emailData.bodyPreview || 'Email content',\n keyAction: 'Review manually',\n replyDraft: '',\n replySubject: '',\n suggestedLabel: '\ud83d\udcac Follow-up',\n shouldAutoArchive: false,\n sentimentTone: 'neutral'\n };\n}\n\nreturn {\n json: {\n // Email data\n gmailId: emailData.gmailId,\n threadId: emailData.threadId,\n from: emailData.from,\n senderName: emailData.senderName,\n senderEmail: emailData.senderEmail,\n senderDomain: emailData.senderDomain,\n to: emailData.to,\n subject: emailData.subject,\n date: emailData.date,\n bodyPreview: emailData.bodyPreview,\n hasAttachments: emailData.hasAttachments,\n isReply: emailData.isReply,\n replyTo: emailData.replyTo,\n \n // AI classification\n category: classData.category,\n priority: classData.priority || 2,\n confidence: parseFloat(classData.confidence) || 0,\n reason: classData.reason || '',\n summary: classData.summary || '',\n keyAction: classData.keyAction || '',\n replyDraft: classData.replyDraft || '',\n replySubject: classData.replySubject || `Re: ${emailData.subject}`,\n suggestedLabel: classData.suggestedLabel || '\ud83d\udcac Follow-up',\n shouldAutoArchive: classData.shouldAutoArchive === true,\n sentimentTone: classData.sentimentTone || 'neutral',\n \n // Meta\n processedAt: new Date().toISOString()\n }\n};"
},
"typeVersion": 2
},
{
"id": "24539ed4-1050-4e06-a2d3-3bef0ddaedb9",
"name": "\ud83d\udd00 Route by Category",
"type": "n8n-nodes-base.switch",
"position": [
1344,
1088
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.category }}",
"rightValue": "urgent"
}
]
},
"renameOutput": true
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.category }}",
"rightValue": "action_required"
}
]
},
"renameOutput": true
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.category }}",
"rightValue": "follow_up"
}
]
},
"renameOutput": true
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.category }}",
"rightValue": "newsletter"
}
]
},
"renameOutput": true
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.category }}",
"rightValue": "automated"
}
]
},
"renameOutput": true
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.category }}",
"rightValue": "spam_promo"
}
]
},
"renameOutput": true
}
]
},
"options": {
"allMatchingOutputs": false
}
},
"typeVersion": 3.2
},
{
"id": "1b2f61c6-0e8e-41ed-85b1-7ede8ddf12db",
"name": "\ud83c\udff7\ufe0f Label: Urgent",
"type": "n8n-nodes-base.gmail",
"position": [
1600,
800
],
"parameters": {
"labelIds": [
"YOUR_LABEL_ID_URGENT"
],
"messageId": "={{ $json.gmailId }}",
"operation": "addLabels"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "6698eeaa-a2fd-429e-b552-f09438535441",
"name": "\ud83d\udcdd Draft Reply (Urgent)",
"type": "n8n-nodes-base.gmail",
"position": [
1840,
800
],
"parameters": {
"message": "={{ $json.replyDraft }}",
"options": {
"threadId": "={{ $json.threadId }}"
},
"subject": "={{ $json.replySubject }}",
"resource": "draft"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "d338bc75-a2b5-41bc-96a1-88aa96a1fc25",
"name": "\ud83c\udff7\ufe0f Label: Action Required",
"type": "n8n-nodes-base.gmail",
"position": [
1600,
960
],
"parameters": {
"labelIds": [
"YOUR_LABEL_ID_ACTION_REQUIRED"
],
"messageId": "={{ $json.gmailId }}",
"operation": "addLabels"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "1540bf40-50a9-4cea-8255-27bf0aa0cccd",
"name": "\ud83d\udcdd Draft Reply (Action)",
"type": "n8n-nodes-base.gmail",
"position": [
1840,
960
],
"parameters": {
"message": "={{ $json.replyDraft }}",
"options": {
"threadId": "={{ $json.threadId }}"
},
"subject": "={{ $json.replySubject }}",
"resource": "draft"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "ef532b54-ad93-4805-94a4-af104ef20588",
"name": "\ud83c\udff7\ufe0f Label: Follow-up",
"type": "n8n-nodes-base.gmail",
"position": [
1600,
1120
],
"parameters": {
"labelIds": [
"YOUR_LABEL_ID_FOLLOW_UP"
],
"messageId": "={{ $json.gmailId }}",
"operation": "addLabels"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "c360cea5-623d-45eb-8ce0-c6b7c4db9deb",
"name": "\ud83c\udff7\ufe0f Label: Newsletter",
"type": "n8n-nodes-base.gmail",
"position": [
1600,
1248
],
"parameters": {
"labelIds": [
"YOUR_LABEL_ID_NEWSLETTER"
],
"messageId": "={{ $json.gmailId }}",
"operation": "addLabels"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "4304e340-abe5-4b39-b3de-c07831ed52cf",
"name": "\ud83c\udff7\ufe0f Label: Automated",
"type": "n8n-nodes-base.gmail",
"position": [
1600,
1408
],
"parameters": {
"labelIds": [
"YOUR_LABEL_ID_AUTOMATED"
],
"messageId": "={{ $json.gmailId }}",
"operation": "addLabels"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "139f1b21-d3bf-4e64-bb24-31e752bbde15",
"name": "\ud83c\udff7\ufe0f Label: Spam-Promo",
"type": "n8n-nodes-base.gmail",
"position": [
1600,
1600
],
"parameters": {
"labelIds": [
"YOUR_LABEL_ID_SPAM_PROMO"
],
"messageId": "={{ $json.gmailId }}",
"operation": "addLabels"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "91a158e1-ccc9-4238-a69b-0418d60cb82d",
"name": "\ud83d\udccb Prepare Log Entry",
"type": "n8n-nodes-base.code",
"position": [
2080,
1088
],
"parameters": {
"jsCode": "// Merge all paths back for logging\nconst item = $input.all()[0].json;\n\n// Try to get original classification data from the appropriate upstream node\nlet classificationData = {};\n\ntry {\n classificationData = $('\ud83c\udfaf Extract Classification').item.json;\n} catch (e) {\n classificationData = item;\n}\n\nreturn {\n json: {\n // For Google Sheets logging\n Date: classificationData.date || new Date().toISOString(),\n From: classificationData.from || '',\n Subject: classificationData.subject || '',\n Category: classificationData.category || '',\n Priority: classificationData.priority || '',\n Confidence: classificationData.confidence || '',\n Reason: classificationData.reason || '',\n Summary: classificationData.summary || '',\n 'Key Action': classificationData.keyAction || '',\n 'Has Reply Draft': classificationData.replyDraft ? 'Yes' : 'No',\n Sentiment: classificationData.sentimentTone || '',\n 'Auto Archive': classificationData.shouldAutoArchive ? 'Yes' : 'No',\n 'Gmail ID': classificationData.gmailId || '',\n 'Processed At': classificationData.processedAt || new Date().toISOString()\n }\n};"
},
"typeVersion": 2
},
{
"id": "3518b511-57c9-4d97-aa32-9b79631e8994",
"name": "\ud83d\udcca Log to Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
2384,
1088
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "Date",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "From",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "From",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Subject",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Subject",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Category",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Category",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Priority",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Priority",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Confidence",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Confidence",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Reason",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Reason",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Summary",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Summary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Key Action",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Key Action",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Has Reply Draft",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Has Reply Draft",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Sentiment",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Sentiment",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Auto Archive",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Auto Archive",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Gmail ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Gmail ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Processed At",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Processed At",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 772918340,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=772918340",
"cachedResultName": "log entry"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit",
"cachedResultName": "Email Triage Log"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.6
},
{
"id": "b39e4a70-1ccc-421f-87d6-1ac95fe53c48",
"name": "Ollama Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"position": [
560,
1328
],
"parameters": {
"model": "mistral",
"options": {}
},
"credentials": {
"ollamaApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "00000000-0000-0000-0000-000000000000",
"connections": {
"Ollama Chat Model": {
"ai_languageModel": [
[
{
"node": "\ud83e\udd16 AI Email Classifier",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"\ud83d\udce5 Gmail Trigger": {
"main": [
[
{
"node": "\ud83d\udd27 Extract Email Data",
"type": "main",
"index": 0
}
]
]
},
"\ud83c\udff7\ufe0f Label: Urgent": {
"main": [
[
{
"node": "\ud83d\udcdd Draft Reply (Urgent)",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udccb Prepare Log Entry": {
"main": [
[
{
"node": "\ud83d\udcca Log to Sheet",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd00 Route by Category": {
"main": [
[
{
"node": "\ud83c\udff7\ufe0f Label: Urgent",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83c\udff7\ufe0f Label: Action Required",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83c\udff7\ufe0f Label: Follow-up",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83c\udff7\ufe0f Label: Newsletter",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83c\udff7\ufe0f Label: Automated",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83c\udff7\ufe0f Label: Spam-Promo",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd27 Extract Email Data": {
"main": [
[
{
"node": "\ud83e\udd16 AI Email Classifier",
"type": "main",
"index": 0
}
]
]
},
"\ud83c\udff7\ufe0f Label: Automated": {
"main": [
[
{
"node": "\ud83d\udccb Prepare Log Entry",
"type": "main",
"index": 0
}
]
]
},
"\ud83c\udff7\ufe0f Label: Follow-up": {
"main": [
[
{
"node": "\ud83d\udccb Prepare Log Entry",
"type": "main",
"index": 0
}
]
]
},
"\ud83e\udd16 AI Email Classifier": {
"main": [
[
{
"node": "\ud83c\udfaf Extract Classification",
"type": "main",
"index": 0
}
]
]
},
"\ud83c\udff7\ufe0f Label: Newsletter": {
"main": [
[
{
"node": "\ud83d\udccb Prepare Log Entry",
"type": "main",
"index": 0
}
]
]
},
"\ud83c\udff7\ufe0f Label: Spam-Promo": {
"main": [
[
{
"node": "\ud83d\udccb Prepare Log Entry",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcdd Draft Reply (Action)": {
"main": [
[
{
"node": "\ud83d\udccb Prepare Log Entry",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcdd Draft Reply (Urgent)": {
"main": [
[
{
"node": "\ud83d\udccb Prepare Log Entry",
"type": "main",
"index": 0
}
]
]
},
"\ud83c\udfaf Extract Classification": {
"main": [
[
{
"node": "\ud83d\udd00 Route by Category",
"type": "main",
"index": 0
}
]
]
},
"\ud83c\udff7\ufe0f Label: Action Required": {
"main": [
[
{
"node": "\ud83d\udcdd Draft Reply (Action)",
"type": "main",
"index": 0
}
]
]
}
}
}
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.
gmailOAuth2googleSheetsOAuth2ApiollamaApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow is built for professionals, founders, freelancers, and anyone drowning in email who wants to automatically triage their inbox using AI — sorting emails into categories, applying Gmail labels, and generating reply drafts for important messages, all running locally…
Source: https://n8n.io/workflows/13463/ — 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.
Turn a simple email workflow into a LinkedIn content machine. Generate post ideas, draft full posts, and auto-publish to LinkedIn all controlled by replying to emails.
This workflow is for hotel managers, travel agencies, and hospitality teams who receive booking requests via email. It eliminates the need for manual data entry by automatically parsing emails and att
Enterprise-grade resume screening automation built for production environments. This workflow combines intelligent AI analysis with comprehensive error handling to ensure reliable processing of candid
AI Agents Vs AI Workflow. Uses lmChatOpenAi, gmailTrigger, gmail, gmailTool. Event-driven trigger; 30 nodes.
*Tags: AI Agent, Supply Chain, Logistics, Circular Economy, Route Planning, Transportation, GPS API*