This workflow corresponds to n8n.io template #14967 — we link there as the canonical source.
This workflow follows the Agent → Gmail Trigger 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 →
{
"nodes": [
{
"id": "1c69224e-4969-4db1-a974-6023baec83ed",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1120,
-368
],
"parameters": {
"width": 540,
"height": 1040,
"content": "## Customer Complaint Triage \u2014 Gmail + GPT-4o-mini + Slack + Google Sheets\n\nFor support teams and customer success managers who receive complaint emails and want automatic AI triage, department routing, and a ready-to-send reply draft \u2014 without manual review. Gmail is polled every minute. Each email is scanned for complaint keywords. If a complaint is detected, GPT-4o-mini scores it from 1 to 10, classifies it as Billing, Technical, or General, writes a one-line summary, and drafts a reply. The complaint is logged to Google Sheets and a formatted Slack alert is sent to the correct department channel automatically.\n\n## How it works\n- **1. Gmail \u2014 Inbox Monitor** polls Gmail every minute for new emails\n- **2. Set \u2014 Config Values** stores company name, Sheet ID, sheet tab name, three Slack channel names, and support email\n- **3. Code \u2014 Extract Email Fields** parses sender name, sender email, subject, and body from the raw Gmail payload\n- **4. IF \u2014 Is This a Complaint?** scans subject and body for 12 complaint keywords \u2014 non-complaints exit cleanly\n- **5. AI Agent \u2014 Triage Complaint** uses GPT-4o-mini to return urgency score, department, one-line summary, and a reply draft\n- **8. Code \u2014 Combine Triage Data** merges AI output with email data, sets urgency label, and picks the right Slack channel\n- **9. Google Sheets \u2014 Log Complaint** appends a 12-column row to the complaint log\n- **10. Slack \u2014 Send Department Alert** posts the full triage to the correct Billing, Technical, or General channel\n\n## Set up steps\n1. In **1. Gmail \u2014 Inbox Monitor** \u2014 connect your Gmail OAuth2 credential\n2. In **2. Set \u2014 Config Values** \u2014 replace all seven values: company name, Sheet ID, sheet tab name, three Slack channel names, and support email\n3. In **6. OpenAI \u2014 GPT-4o-mini Model** \u2014 connect your OpenAI credential\n4. In **9. Google Sheets \u2014 Log Complaint** \u2014 connect your Google Sheets OAuth2 credential and create a sheet tab named Complaint Log\n5. In **10. Slack \u2014 Send Department Alert** \u2014 connect your Slack OAuth2 credential and invite the bot to all three channels\n6. Activate the workflow \u2014 it will poll Gmail every minute automatically"
},
"typeVersion": 1
},
{
"id": "795d09a4-b170-4708-bb17-3ac47c1c456c",
"name": "Section \u2014 Inbox Monitoring and Config",
"type": "n8n-nodes-base.stickyNote",
"position": [
-560,
-96
],
"parameters": {
"color": 5,
"width": 436,
"height": 340,
"content": "## Inbox Monitoring and Config\nGmail is polled every minute for new emails. Config stores company name, Sheet ID, three Slack channel names, and support email \u2014 all set once here."
},
"typeVersion": 1
},
{
"id": "d59bd405-aeb0-4038-9d2e-2246fa818758",
"name": "Section \u2014 Email Parsing and Complaint Detection",
"type": "n8n-nodes-base.stickyNote",
"position": [
-112,
-176
],
"parameters": {
"color": 6,
"width": 420,
"height": 660,
"content": "## Email Parsing and Complaint Detection\nExtracts sender details, subject, and body from the raw Gmail payload. Scans subject and body for 12 complaint keywords. Non-complaint emails exit cleanly via the NoOp node."
},
"typeVersion": 1
},
{
"id": "413d62c5-80d1-42d4-b85d-af8598fee9e8",
"name": "Section \u2014 AI Triage Analysis",
"type": "n8n-nodes-base.stickyNote",
"position": [
336,
-176
],
"parameters": {
"color": 6,
"width": 404,
"height": 708,
"content": "## AI Triage Analysis\nGPT-4o-mini returns four structured fields: urgency score (1\u201310), department (Billing, Technical, or General), one-line summary, and a ready-to-send reply draft. Structured Output Parser enforces the schema."
},
"typeVersion": 1
},
{
"id": "e682a744-daf2-4561-af6a-927053658caa",
"name": "Section \u2014 Data Assembly",
"type": "n8n-nodes-base.stickyNote",
"position": [
752,
-176
],
"parameters": {
"color": 6,
"height": 420,
"content": "## Data Assembly\nMerges AI output with original email data. Sets urgency label (Low, Medium, High, CRITICAL) based on score. Routes to the correct Slack channel based on department."
},
"typeVersion": 1
},
{
"id": "04929e03-875a-482c-9772-706064c00c73",
"name": "Section \u2014 Complaint Log and Department Alert",
"type": "n8n-nodes-base.stickyNote",
"position": [
1040,
-320
],
"parameters": {
"color": 4,
"width": 408,
"height": 804,
"content": "## Complaint Log and Department Alert\nGoogle Sheets appends a 12-column permanent record for every complaint. Slack posts a formatted alert with urgency, summary, and reply draft to the correct department channel."
},
"typeVersion": 1
},
{
"id": "8706d36d-70cf-4103-b30f-da979a75e4d5",
"name": "1. Gmail \u2014 Inbox Monitor",
"type": "n8n-nodes-base.gmailTrigger",
"position": [
-512,
80
],
"parameters": {
"simple": false,
"filters": {},
"options": {},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
}
},
"typeVersion": 1.3
},
{
"id": "60b70aa9-11ce-4cda-8a10-360039a2bfe1",
"name": "2. Set \u2014 Config Values",
"type": "n8n-nodes-base.set",
"position": [
-288,
80
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-001",
"name": "companyName",
"type": "string",
"value": "YOUR COMPANY NAME"
},
{
"id": "cfg-002",
"name": "sheetId",
"type": "string",
"value": "YOUR_GOOGLE_SHEET_ID"
},
{
"id": "cfg-003",
"name": "sheetName",
"type": "string",
"value": "Complaint Log"
},
{
"id": "cfg-004",
"name": "slackBillingChannel",
"type": "string",
"value": "#billing-support"
},
{
"id": "cfg-005",
"name": "slackTechChannel",
"type": "string",
"value": "#tech-support"
},
{
"id": "cfg-006",
"name": "slackSupportChannel",
"type": "string",
"value": "#customer-support"
},
{
"id": "cfg-007",
"name": "supportEmail",
"type": "string",
"value": "user@example.com"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "f6b912a1-de08-42b3-9624-ae5b1f1fb998",
"name": "3. Code \u2014 Extract Email Fields",
"type": "n8n-nodes-base.code",
"position": [
-64,
80
],
"parameters": {
"jsCode": "// Extract clean fields from Gmail trigger output\nconst email = $input.first().json;\nconst config = $('2. Set \u2014 Config Values').item.json;\n\n// Get sender details\nconst fromRaw = email.from?.text || email.from || '';\nconst senderEmail = fromRaw.match(/[\\w.-]+@[\\w.-]+\\.[a-z]{2,}/i)?.[0] || 'user@example.com';\nconst senderName = fromRaw.replace(/<.*?>/, '').trim() || 'Unknown Sender';\n\n// Get email content\nconst subject = email.subject || 'No Subject';\nconst body = email.text || email.snippet || email.body?.text || 'No email body found';\nconst receivedAt = email.date || new Date().toISOString();\nconst emailId = email.id || email.messageId || 'unknown';\n\n// Truncate body to 3000 chars to stay within token limits\nconst bodyTruncated = body.substring(0, 3000);\n\nreturn [{\n json: {\n emailId,\n senderEmail,\n senderName,\n subject,\n emailBody: bodyTruncated,\n receivedAt,\n companyName: config.companyName,\n sheetId: config.sheetId,\n sheetName: config.sheetName,\n slackBillingChannel: config.slackBillingChannel,\n slackTechChannel: config.slackTechChannel,\n slackSupportChannel: config.slackSupportChannel,\n supportEmail: config.supportEmail\n }\n}];"
},
"typeVersion": 2
},
{
"id": "13aca007-fe69-40fb-9cfc-0d05d69d6695",
"name": "4. IF \u2014 Is This a Complaint?",
"type": "n8n-nodes-base.if",
"position": [
160,
80
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "cond-complaint-001",
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $json.subject.toLowerCase().includes('complaint') || $json.subject.toLowerCase().includes('issue') || $json.subject.toLowerCase().includes('problem') || $json.subject.toLowerCase().includes('not working') || $json.subject.toLowerCase().includes('broken') || $json.subject.toLowerCase().includes('refund') || $json.subject.toLowerCase().includes('angry') || $json.subject.toLowerCase().includes('disappointed') || $json.emailBody.toLowerCase().includes('complaint') || $json.emailBody.toLowerCase().includes('very unhappy') || $json.emailBody.toLowerCase().includes('unacceptable') || $json.emailBody.toLowerCase().includes('demand refund') || $json.emailBody.toLowerCase().includes('worst') }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "e4d88977-7da4-4b7d-b1ce-1b6bf588b83c",
"name": "5. AI Agent \u2014 Triage Complaint",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
368,
64
],
"parameters": {
"text": "=You are a customer support triage specialist at {{ $json.companyName }}.\n\nYou must analyze this customer complaint email and return a JSON object with exactly 4 fields.\n\nCUSTOMER EMAIL:\nFrom: {{ $json.senderName }} ({{ $json.senderEmail }})\nSubject: {{ $json.subject }}\nBody: {{ $json.emailBody }}\n\nYou must return ONLY a valid JSON object with these 4 fields:\n\nurgencyScore \u2014 a number from 1 to 10 based on how urgent this complaint is.\n1 = minor inconvenience, 5 = moderate issue, 8 = very upset customer, 10 = threatening legal action or churn\n\ndepartment \u2014 which team should handle this complaint. Choose exactly one of these:\nBilling (payment issues, refund requests, invoice problems, overcharge)\nTechnical (product not working, bugs, errors, login issues, integration problems)\nGeneral (shipping, delivery, service quality, general dissatisfaction)\n\noneLineSummary \u2014 one plain sentence (under 20 words) summarizing what the customer is complaining about.\n\nsuggestedReply \u2014 a professional, empathetic reply the support agent can send to this customer. Keep it under 80 words. Plain text only. Start with the customer name. End with your company name.\n\nRULES:\n- Return ONLY the JSON object. No extra text before or after.\n- No markdown. No code blocks. No backticks.\n- urgencyScore must be a number, not a string.\n- department must be exactly: Billing OR Technical OR General",
"options": {},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.7
},
{
"id": "fdf62aad-ba06-4337-ab38-3701bda73503",
"name": "6. OpenAI \u2014 GPT-4o-mini Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
368,
288
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini"
},
"options": {
"maxTokens": 600,
"temperature": 0.3
}
},
"typeVersion": 1.2
},
{
"id": "3e39122b-e6f4-4a52-87b7-3704e4a617b6",
"name": "7. Parser \u2014 Structured Triage Output",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
512,
384
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"urgencyScore\": {\n \"type\": \"number\",\n \"description\": \"Urgency score from 1 to 10. 1 = minor, 10 = critical\"\n },\n \"department\": {\n \"type\": \"string\",\n \"description\": \"Which department: Billing OR Technical OR General\"\n },\n \"oneLineSummary\": {\n \"type\": \"string\",\n \"description\": \"One sentence summary of the complaint under 20 words\"\n },\n \"suggestedReply\": {\n \"type\": \"string\",\n \"description\": \"Professional empathetic reply under 80 words for the support agent to send\"\n }\n },\n \"required\": [\"urgencyScore\", \"department\", \"oneLineSummary\", \"suggestedReply\"]\n}"
},
"typeVersion": 1.3
},
{
"id": "8e255e4e-9b22-47bc-ab36-54374b6db966",
"name": "8. Code \u2014 Combine Triage Data",
"type": "n8n-nodes-base.code",
"position": [
816,
64
],
"parameters": {
"jsCode": "// Combine AI triage output with original email data\nconst aiOutput = $input.first().json.output;\nconst emailData = $('3. Code \u2014 Extract Email Fields').item.json;\n\nconst urgencyScore = aiOutput.urgencyScore || 0;\nconst department = aiOutput.department || 'General';\nconst oneLineSummary = aiOutput.oneLineSummary || 'No summary available';\nconst suggestedReply = aiOutput.suggestedReply || 'No reply generated';\n\n// Set urgency label based on score\nlet urgencyLabel = 'Low';\nif (urgencyScore >= 8) urgencyLabel = 'CRITICAL';\nelse if (urgencyScore >= 6) urgencyLabel = 'High';\nelse if (urgencyScore >= 4) urgencyLabel = 'Medium';\n\n// Decide which Slack channel based on department\nlet slackChannel = emailData.slackSupportChannel;\nif (department === 'Billing') slackChannel = emailData.slackBillingChannel;\nif (department === 'Technical') slackChannel = emailData.slackTechChannel;\n\n// Format received date nicely\nconst receivedDate = emailData.receivedAt\n ? new Date(emailData.receivedAt).toISOString().replace('T', ' ').substring(0, 16)\n : new Date().toISOString().replace('T', ' ').substring(0, 16);\n\nreturn [{\n json: {\n emailId: emailData.emailId,\n senderEmail: emailData.senderEmail,\n senderName: emailData.senderName,\n subject: emailData.subject,\n emailBody: emailData.emailBody,\n receivedDate,\n supportEmail: emailData.supportEmail,\n sheetId: emailData.sheetId,\n sheetName: emailData.sheetName,\n urgencyScore,\n urgencyLabel,\n department,\n oneLineSummary,\n suggestedReply,\n slackChannel,\n loggedAt: new Date().toISOString().replace('T', ' ').substring(0, 16)\n }\n}];"
},
"typeVersion": 2
},
{
"id": "577af46e-295f-4cb9-a9a3-74ebac8884b0",
"name": "9. Google Sheets \u2014 Log Complaint",
"type": "n8n-nodes-base.googleSheets",
"position": [
1152,
-144
],
"parameters": {
"columns": {
"value": {
"Subject": "={{ $json.subject }}",
"Email ID": "={{ $json.emailId }}",
"Logged At": "={{ $json.loggedAt }}",
"Department": "={{ $json.department }}",
"Sender Name": "={{ $json.senderName }}",
"Sender Email": "={{ $json.senderEmail }}",
"Received Date": "={{ $json.receivedDate }}",
"Urgency Level": "={{ $json.urgencyLabel }}",
"Urgency Score": "={{ $json.urgencyScore }}",
"Suggested Reply": "={{ $json.suggestedReply }}",
"One Line Summary": "={{ $json.oneLineSummary }}",
"Slack Channel Alerted": "={{ $json.slackChannel }}"
},
"mappingMode": "defineBelow"
},
"options": {
"cellFormat": "USER_ENTERED"
},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $json.sheetName }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.sheetId }}"
}
},
"typeVersion": 4.5
},
{
"id": "26bcc247-487c-4b2a-b9c8-714dda812a1b",
"name": "10. Slack \u2014 Send Department Alert",
"type": "n8n-nodes-base.slack",
"position": [
1200,
208
],
"parameters": {
"text": "=*NEW COMPLAINT ALERT \u2014 {{ $json.urgencyLabel }}*\n\n*Urgency Score:* {{ $json.urgencyScore }}/10\n*Department:* {{ $json.department }}\n*From:* {{ $json.senderName }} ({{ $json.senderEmail }})\n*Subject:* {{ $json.subject }}\n*Received:* {{ $json.receivedDate }}\n\n*Summary:*\n{{ $json.oneLineSummary }}\n\n*Suggested Reply for Agent:*\n{{ $json.suggestedReply }}\n\n---\n_Reply to: {{ $json.senderEmail }} | Support: {{ $json.supportEmail }}_\n_Logged to Google Sheets automatically_",
"otherOptions": {
"mrkdwn": true
},
"authentication": "oAuth2"
},
"typeVersion": 2.2
},
{
"id": "71672e57-90b1-43e0-a045-4944b24d800e",
"name": "11. NoOp \u2014 Not a Complaint",
"type": "n8n-nodes-base.noOp",
"position": [
160,
288
],
"parameters": {},
"typeVersion": 1
}
],
"connections": {
"2. Set \u2014 Config Values": {
"main": [
[
{
"node": "3. Code \u2014 Extract Email Fields",
"type": "main",
"index": 0
}
]
]
},
"1. Gmail \u2014 Inbox Monitor": {
"main": [
[
{
"node": "2. Set \u2014 Config Values",
"type": "main",
"index": 0
}
]
]
},
"4. IF \u2014 Is This a Complaint?": {
"main": [
[
{
"node": "5. AI Agent \u2014 Triage Complaint",
"type": "main",
"index": 0
}
],
[
{
"node": "11. NoOp \u2014 Not a Complaint",
"type": "main",
"index": 0
}
]
]
},
"6. OpenAI \u2014 GPT-4o-mini Model": {
"ai_languageModel": [
[
{
"node": "5. AI Agent \u2014 Triage Complaint",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"8. Code \u2014 Combine Triage Data": {
"main": [
[
{
"node": "9. Google Sheets \u2014 Log Complaint",
"type": "main",
"index": 0
},
{
"node": "10. Slack \u2014 Send Department Alert",
"type": "main",
"index": 0
}
]
]
},
"3. Code \u2014 Extract Email Fields": {
"main": [
[
{
"node": "4. IF \u2014 Is This a Complaint?",
"type": "main",
"index": 0
}
]
]
},
"5. AI Agent \u2014 Triage Complaint": {
"main": [
[
{
"node": "8. Code \u2014 Combine Triage Data",
"type": "main",
"index": 0
}
]
]
},
"7. Parser \u2014 Structured Triage Output": {
"ai_outputParser": [
[
{
"node": "5. AI Agent \u2014 Triage Complaint",
"type": "ai_outputParser",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Activate this workflow once and it monitors your Gmail inbox every minute automatically. Every incoming email is scanned for complaint keywords — and if a complaint is detected, GPT-4o-mini scores its urgency, identifies the right department, writes a one-line summary, and…
Source: https://n8n.io/workflows/14967/ — 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.
This workflow automates invoice processing directly from your email inbox.
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
Streamline your HR recruitment process with this intelligent automation that reads candidate emails and resumes, analyzes them using GPT-4, and automatically shortlists or rejects applicants based on
AI-Powered Invoice Processing from Gmail to Google Sheets with Slack Approval This workflow completely automates your invoice processing pipeline. It triggers when a new invoice email arrives in Gmail
Consultants, agencies, financial analysts, and project managers who regularly receive client PDFs, invoices, or reports.