This workflow corresponds to n8n.io template #15835 — we link there as the canonical source.
This workflow follows the Agent → Form 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": "b395a161-0234-4725-a4bf-b0537bec7422",
"name": "Records",
"type": "n8n-nodes-base.googleSheets",
"position": [
1696,
128
],
"parameters": {
"columns": {
"value": {
"name": "={{ $('Normalize Channel Inputs').item.json.name }}",
"time": "={{ $json.receivedAt }}",
"issue": "={{ $('Normalize Channel Inputs').item.json.subject }}",
"E-mail": "={{ $('Normalize Channel Inputs').item.json.email }}",
"drafted solution": "={{ $json.auto_reply_draft }}"
},
"schema": [
{
"id": "name",
"type": "string",
"display": true,
"required": false,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "E-mail",
"type": "string",
"display": true,
"required": false,
"displayName": "E-mail",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "issue",
"type": "string",
"display": true,
"required": false,
"displayName": "issue",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "drafted solution",
"type": "string",
"display": true,
"required": false,
"displayName": "drafted solution",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "time",
"type": "string",
"display": true,
"required": false,
"displayName": "time",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/17AxsE2AA4qcUDu6TL5pzy-UPssAp0kTbE5Atdkvlku4/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "17AxsE2AA4qcUDu6TL5pzy-UPssAp0kTbE5Atdkvlku4",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/17AxsE2AA4qcUDu6TL5pzy-UPssAp0kTbE5Atdkvlku4/edit?usp=drivesdk",
"cachedResultName": "csx"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "ad782560-9321-4346-88aa-ae510ea65ad9",
"name": "AI Agent \u2013 Triage",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
800,
272
],
"parameters": {
"text": "=Analyze the following support ticket and return ONLY a valid JSON object.\n\nTicket ID: {{ $json.ticketId }}\nSource Channel: {{ $json.channel }}\nCustomer Name: {{ $json.name }}\nCustomer Email: {{ $json.email }}\nPhone: {{ $json.phone || 'Not provided' }}\nSubject / Issue Category: {{ $json.subject }}\nMessage: {{ $json.message }}\nReceived At: {{ $json.receivedAt }}",
"options": {
"systemMessage": "You are a customer support triage specialist. Analyze support tickets and return a structured JSON response.\n\nCLASSIFICATION RULES:\n\nTOPIC (choose one):\n- billing: payment, invoice, charge, subscription, pricing\n- technical: bug, error, not working, integration, crash, slow\n- shipping: delivery, tracking, lost, damaged, delay\n- refund: return, money back, cancellation, chargeback\n- account: login, password, access, profile, verification\n- general: anything else\n\nURGENCY (choose one):\n- critical: service down, payment failed, data loss, legal threat, security breach\n- high: major issue blocking the customer, cannot use the product\n- medium: important but not blocking, needs response within 24 hours\n- low: general question, feedback, minor inconvenience\n\nSENTIMENT (choose one):\n- angry: hostile, aggressive, threatening language\n- frustrated: unhappy but not aggressive, clearly dissatisfied\n- neutral: calm, factual, no emotional charge\n- satisfied: positive despite having an issue\n\nTEAM ROUTING:\n- billing_team: billing or refund topics\n- tech_support: technical topics\n- logistics: shipping topics\n- customer_success: account or general topics\n\nRETURN EXACTLY this JSON structure with no markdown, no code fences, no extra text:\n{\n \"topic\": \"<topic>\",\n \"urgency\": \"<urgency>\",\n \"sentiment\": \"<sentiment>\",\n \"summary\": \"<1-2 sentence summary of the customer issue, written in third person>\",\n \"suggested_team\": \"<team>\",\n \"auto_reply_draft\": \"<short, warm, professional reply addressed to the customer by first name. Never mention internal team names. Acknowledge the specific issue and set a response time expectation based on urgency: critical=1h, high=4h, medium=24h, low=48h>\"\n}"
},
"promptType": "define"
},
"typeVersion": 3.1
},
{
"id": "34e67a76-000c-46ae-9579-c888de7f9a56",
"name": "Gmail Trigger",
"type": "n8n-nodes-base.gmailTrigger",
"position": [
272,
144
],
"parameters": {
"filters": {},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
}
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 1.4
},
{
"id": "403b014d-7f34-48f6-a183-c6df88837c98",
"name": "Form Trigger",
"type": "n8n-nodes-base.formTrigger",
"position": [
272,
384
],
"parameters": {
"options": {},
"formTitle": "Customer Query Form",
"formFields": {
"values": [
{
"fieldLabel": "name",
"requiredField": true
},
{
"fieldType": "email",
"fieldLabel": "E-mail",
"requiredField": true
},
{
"fieldType": "number",
"fieldLabel": "Phone Number"
},
{
"fieldType": "dropdown",
"fieldLabel": "Issue",
"fieldOptions": {
"values": [
{
"option": "Billing"
},
{
"option": "Technical"
},
{
"option": "Shipping"
},
{
"option": "Refund"
},
{
"option": "Account"
},
{
"option": "General"
}
]
},
"requiredField": true
},
{
"fieldType": "textarea",
"fieldLabel": "Message"
}
]
},
"formDescription": "Please describe your situation so we can help you faster."
},
"typeVersion": 2.5
},
{
"id": "30e00c27-ef12-4109-b11b-6a702ca7582a",
"name": "Normalize Channel Inputs",
"type": "n8n-nodes-base.code",
"position": [
528,
272
],
"parameters": {
"jsCode": "// ============================================================\n// 03 | Normalize All Channel Inputs\n// Unifies Gmail and Form data into a single clean ticket shape\n// ============================================================\nconst item = $input.first();\nconst json = item.json;\n\n// Detect channel source\nconst isForm = json.submittedAt !== undefined || json['E-mail'] !== undefined;\nconst isEmail = json.From !== undefined || json.snippet !== undefined;\n\n// Extract name\nlet name = 'Unknown';\nif (isForm) {\n name = (json.name || '').trim() || 'Unknown';\n} else if (isEmail) {\n // Gmail From field format: \"Display Name <user@example.com>\"\n const fromRaw = json.From || '';\n name = fromRaw.includes('<')\n ? fromRaw.split('<')[0].trim().replace(/\"/g, '')\n : fromRaw.split('@')[0] || 'Unknown';\n}\n\n// Extract email\nlet email = '';\nif (isForm) {\n email = (json['E-mail'] || '').trim().toLowerCase();\n} else if (isEmail) {\n const toField = json.To || '';\n const fromField = json.From || '';\n // Prefer extracting sender email from From field\n const emailMatch = fromField.match(/[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}/);\n email = emailMatch ? emailMatch[0].toLowerCase() : '';\n}\n\n// Extract the issue/message content\nlet message = '';\nif (isForm) {\n const category = json.Issue || 'General';\n const detail = json.Message || json.message || '';\n message = detail ? `Issue Category: ${category}. Details: ${detail}` : `Issue Category: ${category}`;\n} else if (isEmail) {\n const subject = json.Subject || '';\n const snippet = json.snippet || '';\n message = snippet || subject || 'No message body provided';\n}\n\n// Build clean ticket\nconst ticketId = `TKT-${Date.now()}`;\nconst channel = isForm ? 'form' : 'email';\nconst receivedAt = json.submittedAt\n || (json.internalDate ? new Date(parseInt(json.internalDate)).toISOString() : null)\n || new Date().toISOString();\n\nreturn [{\n json: {\n ticketId,\n channel,\n name,\n email,\n message,\n subject: isEmail ? (json.Subject || '') : (json.Issue || ''),\n phone: isForm ? String(json['Phone Number'] || '') : '',\n receivedAt\n }\n}];"
},
"typeVersion": 2
},
{
"id": "fd566078-162c-4dc6-b443-3c4e69376821",
"name": "Parse AI + Merge Ticket",
"type": "n8n-nodes-base.code",
"position": [
1248,
320
],
"parameters": {
"jsCode": "// ============================================================\n// 05 | Parse AI Output + Merge Ticket Data\n// Fixes single-quote JSON, strips markdown, merges both data\n// sources into one complete ticket object for all downstream nodes\n// ============================================================\nconst item = $input.first();\nconst json = item.json;\n\n// The AI Agent places its text response in json.output\nlet rawOutput = (json.output || '').trim();\n\nlet aiResult = null;\n\n// --- Attempt 1: Strip markdown fences and parse directly ---\ntry {\n const stripped = rawOutput\n .replace(/^```json\\s*/i, '')\n .replace(/^```\\s*/i, '')\n .replace(/```\\s*$/i, '')\n .trim();\n aiResult = JSON.parse(stripped);\n} catch (e1) {\n // --- Attempt 2: Fix single-quoted JSON from the AI ---\n try {\n const fixedQuotes = rawOutput\n .replace(/^```json\\s*/i, '')\n .replace(/^```\\s*/i, '')\n .replace(/```\\s*$/i, '')\n .trim()\n // Replace single-quoted string values\n .replace(/:\\s*'([^']*)'/g, ': \"$1\"')\n // Replace single-quoted object keys at start\n .replace(/\\{\\s*'/g, '{ \"')\n // Replace single-quoted keys after comma\n .replace(/,\\s*'/g, ', \"')\n // Close single-quoted keys before colon\n .replace(/'\\s*:/g, '\":');\n aiResult = JSON.parse(fixedQuotes);\n } catch (e2) {\n // --- Attempt 3: Regex extraction as last resort ---\n const extract = (key) => {\n const pattern = new RegExp(\n `[\"']?${key}[\"']?\\\\s*:\\\\s*[\"']([^\"'\\\\n]+)[\"']`,\n 'i'\n );\n const match = rawOutput.match(pattern);\n return match ? match[1].trim() : '';\n };\n aiResult = {\n topic: extract('topic') || 'general',\n urgency: extract('urgency') || 'medium',\n sentiment: extract('sentiment') || 'neutral',\n summary: extract('summary') || 'Could not parse AI response.',\n suggested_team: extract('suggested_team') || 'customer_success',\n auto_reply_draft: extract('auto_reply_draft') || 'Thank you for reaching out. Our team will be in touch shortly.'\n };\n }\n}\n\n// Guard against null result\nif (!aiResult) {\n aiResult = {\n topic: 'general',\n urgency: 'medium',\n sentiment: 'neutral',\n summary: 'Unable to classify this ticket.',\n suggested_team: 'customer_success',\n auto_reply_draft: 'Thank you for contacting us. We will be in touch shortly.'\n };\n}\n\n// Build the complete merged ticket object\n// Prefer original ticketId from normalize step, fall back to AI-generated or timestamp\nconst mergedTicket = {\n // Core identity (from normalize node)\n ticketId: json.ticketId || `TKT-${Date.now()}`,\n channel: json.channel || 'unknown',\n name: json.name || 'Unknown',\n email: json.email || '',\n phone: json.phone || '',\n message: json.message || '',\n subject: json.subject || '',\n receivedAt: json.receivedAt || new Date().toISOString(),\n\n // AI classifications\n topic: aiResult.topic || 'general',\n urgency: aiResult.urgency || 'medium',\n sentiment: aiResult.sentiment || 'neutral',\n summary: aiResult.summary || '',\n suggested_team: aiResult.suggested_team || 'customer_success',\n auto_reply_draft: aiResult.auto_reply_draft || '',\n\n // Derived helpers\n urgencyLabel: (aiResult.urgency || 'medium').toUpperCase(),\n teamLabel: (aiResult.suggested_team || 'customer_success').replace(/_/g, ' ')\n};\n\nreturn [{ json: mergedTicket }];"
},
"typeVersion": 2
},
{
"id": "50535aff-4d4f-4656-b39a-ca671143764f",
"name": "Slack \u2013 Notify Team",
"type": "n8n-nodes-base.slack",
"position": [
1696,
304
],
"parameters": {
"text": "=:ticket: *New Support Ticket \u2014 {{ $json.urgencyLabel }} PRIORITY*\n\n*Ticket ID:* `{{ $json.ticketId }}`\n*Channel:* {{ $json.channel }}\n*Customer:* {{ $json.name }}{{ $json.email ? ' (' + $json.email + ')' : '' }}\n*Topic:* {{ $json.topic }}\n*Urgency:* {{ $json.urgency }}\n*Sentiment:* {{ $json.sentiment }}\n\n*Summary:*\n> {{ $json.summary }}\n\n*Assigned To:* {{ $json.teamLabel }}\n*Received:* {{ $json.receivedAt }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0ASJMR4MMJ",
"cachedResultName": "sales-team"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "de10bf4f-00ae-4447-af25-5437df28124a",
"name": "Gmail \u2013 Auto-Reply",
"type": "n8n-nodes-base.gmail",
"position": [
1680,
496
],
"parameters": {
"sendTo": "={{ $('Gmail Trigger').item.json.To }}",
"message": "=Hi {{ $json.name.split(' ')[0] }},\n\nThank you for reaching out to us. We have received your message and created a support ticket for you.\n\n\u2014 Ticket ID: {{ $json.ticketId }}\n\u2014 Topic: {{ $json.topic }}\n\u2014 Priority: {{ $json.urgency }}\n\n{{ $json.auto_reply_draft }}\n\nIf you need to follow up, please reply to this email and quote your ticket ID above.\n\nWarm regards,\nCustomer Support Team",
"options": {},
"subject": "=We received your request "
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "9b9d3fcd-8e3c-475c-a8fd-77a4baa07f6e",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
816,
448
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini"
},
"options": {
"temperature": 0.2
},
"builtInTools": {}
},
"credentials": {},
"typeVersion": 1.3
},
{
"id": "f953c7f6-ab03-49f9-af0d-41778986172a",
"name": "Main Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-512,
-128
],
"parameters": {
"color": 7,
"width": 560,
"height": 852,
"content": "## \ud83c\udfab AI Customer Support Triage\n\nAutomatically classifies incoming customer support requests from email and web forms, then routes them to the right team with a friendly auto reply and a logged record.\n\n**Perfect for:** Customer support teams, ops leads, and small SaaS founders who want every ticket triaged, routed, and acknowledged within seconds.\n\n***\n\n## How it works\n\n1. **Gmail Trigger** \u00b7 Watches a support inbox and fires on every new email.\n2. **Form Trigger** \u00b7 Catches submissions from a public contact form.\n3. **Normalize Channel Inputs** \u00b7 Merges email and form payloads into one common schema (channel, sender, subject, body).\n4. **AI Agent \u00b7 Triage** \u00b7 Reads the normalized message and returns priority, category, sentiment, and a suggested reply.\n5. **OpenAI Chat Model** \u00b7 Powers the AI Agent with reasoning over the ticket content.\n6. **Parse AI + Merge Ticket** \u00b7 Extracts the structured AI output and combines it with the original ticket metadata.\n7. **Slack \u00b7 Notify Team** \u00b7 Posts a formatted ticket alert to the support channel with priority and category tags.\n8. **Records** \u00b7 Appends a full row of ticket data to a Google Sheets log for reporting.\n9. **Gmail \u00b7 Auto Reply** \u00b7 Sends the customer a polite acknowledgement with their ticket reference.\n\n***\n\n## Setup (~10 minutes)\n\n1. **Gmail OAuth** \u00b7 Connect your support inbox in the *Gmail Trigger* and *Gmail \u00b7 Auto Reply* nodes.\n2. **Form Trigger URL** \u00b7 Copy the production URL from the *Form Trigger* node and embed it in your website.\n3. **OpenAI API** \u00b7 Add your key in the *OpenAI Chat Model* node and confirm the model name matches your plan.\n4. **Slack OAuth** \u00b7 Authorize the workspace and pick the support channel in the *Slack \u00b7 Notify Team* node.\n5. **Google Sheets** \u00b7 Connect the account and point the *Records* node to your tickets spreadsheet with columns matching the AI output schema.\n\n> Test with a sample email and a sample form submission before going live. The AI categories and priorities are defined in the *AI Agent \u00b7 Triage* system prompt, so tune them to match your team's taxonomy."
},
"typeVersion": 1
},
{
"id": "3bd95846-9858-4359-939b-45baf5ff4ee0",
"name": "Section 1 Intake",
"type": "n8n-nodes-base.stickyNote",
"position": [
80,
-16
],
"parameters": {
"color": 5,
"width": 616,
"height": 536,
"content": "## 1\ufe0f\u20e3 Intake \u00b7 Multi Channel Capture\n\nTickets arrive through two parallel doors. The **Gmail Trigger** listens for new messages in your support inbox while the **Form Trigger** catches submissions from a hosted contact form. Both streams flow into **Normalize Channel Inputs**, which collapses their different shapes into a single unified payload so the rest of the workflow only has to deal with one schema."
},
"typeVersion": 1
},
{
"id": "2a5e90a4-22e0-4adb-886d-cf428e3e93e0",
"name": "Section 2 AI Triage",
"type": "n8n-nodes-base.stickyNote",
"position": [
720,
-16
],
"parameters": {
"color": 3,
"width": 392,
"height": 584,
"content": "## 2\ufe0f\u20e3 AI Triage \u00b7 Classification & Reasoning\n\nThe **AI Agent \u00b7 Triage** reads each normalized ticket and decides the priority, category, and sentiment, plus drafts a suggested reply. It runs on the **OpenAI Chat Model**, which provides the reasoning engine for the agent. The structured response makes the downstream routing decisions deterministic."
},
"typeVersion": 1
},
{
"id": "f758d7c7-07f1-4ad2-9bd5-b18db48075de",
"name": "Section 3 Parse Merge",
"type": "n8n-nodes-base.stickyNote",
"position": [
1136,
112
],
"parameters": {
"color": 6,
"width": 360,
"height": 408,
"content": "## 3\ufe0f\u20e3 Parse & Merge \u00b7 Ticket Assembly\n\nThe **Parse AI + Merge Ticket** node extracts the structured fields returned by the AI Agent and stitches them back together with the original sender, channel, and timestamp. The result is a single clean ticket object ready to be fanned out to multiple destinations."
},
"typeVersion": 1
},
{
"id": "e603deef-5be8-4b27-9db1-cb64ee8d32fe",
"name": "Section 4 Output",
"type": "n8n-nodes-base.stickyNote",
"position": [
1536,
-80
],
"parameters": {
"color": 4,
"width": 376,
"height": 728,
"content": "## 4\ufe0f\u20e3 Output \u00b7 Notify, Log, Reply\n\nThe merged ticket fans out to three destinations in parallel. **Slack \u00b7 Notify Team** posts a formatted alert to the support channel so humans see priority cases instantly. **Records** appends a row to your Google Sheets ticket log for reporting and analytics. **Gmail \u00b7 Auto Reply** sends the customer a polite acknowledgement so they know their request was received."
},
"typeVersion": 1
}
],
"connections": {
"Form Trigger": {
"main": [
[
{
"node": "Normalize Channel Inputs",
"type": "main",
"index": 0
}
]
]
},
"Gmail Trigger": {
"main": [
[
{
"node": "Normalize Channel Inputs",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent \u2013 Triage",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"AI Agent \u2013 Triage": {
"main": [
[
{
"node": "Parse AI + Merge Ticket",
"type": "main",
"index": 0
}
]
]
},
"Parse AI + Merge Ticket": {
"main": [
[
{
"node": "Slack \u2013 Notify Team",
"type": "main",
"index": 0
},
{
"node": "Records",
"type": "main",
"index": 0
},
{
"node": "Gmail \u2013 Auto-Reply",
"type": "main",
"index": 0
}
]
]
},
"Normalize Channel Inputs": {
"main": [
[
{
"node": "AI Agent \u2013 Triage",
"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.
gmailOAuth2googleSheetsOAuth2ApislackOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
How it works
Source: https://n8n.io/workflows/15835/ — 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.
AI Agents Vs AI Workflow. Uses lmChatOpenAi, gmailTrigger, gmail, gmailTool. Event-driven trigger; 30 nodes.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
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