{
  "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
          }
        ]
      ]
    }
  }
}