{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "c6d45206-fea1-407e-8792-86e3a8da02ba",
      "name": "Sticky Note \u2014 Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2176,
        752
      ],
      "parameters": {
        "color": 7,
        "width": 620,
        "height": 600,
        "content": "## Customer Support Email Triage\n### Sovereign AI \u2014 IONOS AI Model Hub\n\n**What this workflow does:**\n-  Watches the inbox for new unread emails\n- Filters to only process emails addressed to support@ or info@\n- Uses IONOS Sovereign AI (Llama 3.3-70B) to classify each email AND write a draft reply\n- Categories: **Spam \u00b7 Sales Lead \u00b7 Tech Support \u00b7 FAQ \u00b7 Billing \u00b7 Other**\n- Creates a Gmail **Draft**\n- Spam emails are silently marked as read (no draft created)\n\n**Setup checklist:**\n- [ ] Gmail OAuth2 credentials configured (for support@ or info@)\n- [ ] IONOS Cloud API token added to credentials (ionosCloudApi)\n- [ ] `@ionos-cloud/n8n-nodes-ionos-cloud` node package installed\n- [ ] Update the Filter node with your actual domain (e.g. support@yourcompany.com)\n\n**Data flow:**\nGmail Trigger \u2192 Filter (support@/info@ only) \u2192 Clean Email \u2192 AI Triage & Draft \u2192 Parse \u2192 Route \u2192 Draft / Archive Spam"
      },
      "typeVersion": 1
    },
    {
      "id": "bd77acc6-90e9-4164-89cc-643603f1820d",
      "name": "New Email (info@ / support@)",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        2912,
        1392
      ],
      "parameters": {
        "filters": {
          "readStatus": "unread"
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6b895e3b-7d8a-4d5a-8605-c8872a65d8a4",
      "name": "Only support@ or info@",
      "type": "n8n-nodes-base.filter",
      "position": [
        3184,
        1392
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "filter-cond-01",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.From }}",
              "rightValue": "support@"
            },
            {
              "id": "filter-cond-02",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "=",
              "rightValue": "info@"
            },
            {
              "id": "bb7aee9c-7633-40c3-b22b-f51ea2221993",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "=",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "ad33e66f-bee1-4abb-817f-7daaabbcbefc",
      "name": "Prepare Email Data",
      "type": "n8n-nodes-base.code",
      "position": [
        3440,
        1392
      ],
      "parameters": {
        "jsCode": "// Extract and clean the raw email fields only \u2014 prompt lives in the AI node\nconst email = $input.first().json;\n\nfunction stripHtml(html) {\n  return (html || '')\n    .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n    .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n    .replace(/<[^>]+>/g, ' ')\n    .replace(/&nbsp;/g, ' ')\n    .replace(/&amp;/g, '&')\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\nfunction removeQuotedText(text) {\n  const markers = [\n    /\\nOn .+?wrote:/s,\n    /\\n-{3,}/,\n    /\\nFrom:\\s+/,\n    /\\n_{3,}/,\n    /\\n>{1,}/m\n  ];\n  let result = text || '';\n  for (const marker of markers) {\n    const idx = result.search(marker);\n    if (idx > 80) { result = result.substring(0, idx); break; }\n  }\n  return result.trim();\n}\n\n// Gmail trigger returns capitalized headers: From, To, Subject, Date\n// Body: prefer full text/html, fall back to snippet\nconst rawBody   = email.text || email.Text || stripHtml(email.html || email.Html || '') || email.snippet || '';\nconst cleanBody = removeQuotedText(rawBody).substring(0, 3500);\n\nconst fromRaw   = email.From || email.from || '';\nconst fromMatch = fromRaw.match(/^(.+?)\\s*<([^>]+)>$/);\nconst fromName  = fromMatch ? fromMatch[1].replace(/\"/g, '').trim() : fromRaw.split('@')[0];\nconst fromEmail = fromMatch ? fromMatch[2].trim() : fromRaw.trim();\n\nreturn [{\n  json: {\n    from:      fromEmail,\n    fromName,\n    subject:   email.Subject  || email.subject  || '(no subject)',\n    date:      email.Date     || email.date      || new Date().toISOString(),\n    threadId:  email.threadId || '',\n    messageId: email.id       || '',\n    toAddress: email.To       || email.to        || '',\n    cleanBody\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "52c13ea7-f25c-4b28-9817-34cc6a01c479",
      "name": "AI Triage & Draft Reply",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        3712,
        1392
      ],
      "parameters": {
        "text": "=You are an expert customer support triage assistant for a technology company.\nAnalyze the email below and return ONLY a valid JSON object \u2014 no markdown, no code fences, no explanation.\n\nCATEGORIES (pick exactly one):\n- \"Spam\"        : unsolicited commercial mail, phishing, bot-generated or clearly irrelevant\n- \"Sales Lead\"  : pricing/demo inquiries, partnership proposals, purchase intent\n- \"Tech Support\": technical problems, bugs, errors, product malfunctions, integration issues\n- \"FAQ\"         : common how-to or product-info questions answered in standard documentation\n- \"Billing\"     : payment issues, invoice requests, refunds, subscription changes\n- \"Other\"       : legitimate emails that don't fit the above\n\nPRIORITY (pick one):\n- \"Urgent\" : system down, data loss, legal/compliance threat, or very angry customer\n- \"High\"   : customer is frustrated or has a time-sensitive issue\n- \"Medium\" : normal business inquiry\n- \"Low\"    : informational, no urgency\n\nDRAFT REPLY RULES:\n- Write the reply in the SAME language as the email\n- Open with a personalized greeting using the sender first name when available\n- Acknowledge their specific situation concisely\n- Provide helpful next steps or a resolution path\n- Close professionally; use [YOUR NAME] as the signature placeholder\n- If category is \"Spam\", set draft_reply to the string \"N/A\"\n\nReturn exactly this JSON structure:\n{\n  \"category\": \"...\",\n  \"priority\": \"...\",\n  \"summary\": \"one sentence describing the email\",\n  \"sentiment\": \"Positive | Neutral | Frustrated | Angry\",\n  \"language\": \"detected language name in English (e.g. French, German, English)\",\n  \"draft_reply\": \"complete reply text ready to review and send\"\n}\n\nEMAIL:\nFrom: {{ $json.fromName }} <{{ $json.from }}>\nTo: {{ $json.toAddress }}\nSubject: {{ $json.subject }}\nDate: {{ $json.date }}\nBody:\n{{ $json.cleanBody }}",
        "promptType": "define"
      },
      "typeVersion": 1.4
    },
    {
      "id": "abc5b3a7-4dd5-4eab-b8a1-730c9b89cecc",
      "name": "IONOS Cloud Chat Model",
      "type": "@ionos-cloud/n8n-nodes-ionos-cloud.ionosCloudChatModel",
      "position": [
        3712,
        1616
      ],
      "parameters": {
        "model": "meta-llama/Llama-3.3-70B-Instruct",
        "options": {}
      },
      "credentials": {
        "ionosCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9c4eb4e7-bff9-4783-9081-600c361f9033",
      "name": "Parse AI Response",
      "type": "n8n-nodes-base.code",
      "position": [
        4048,
        1376
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 Parse the AI JSON response and merge with original email data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst llmOutput  = $input.first().json;\nconst emailData  = $('Prepare Email Data').first().json;\n\n// The LLM Chain can surface the text under different keys depending on n8n version\nconst llmText = llmOutput.output || llmOutput.text || llmOutput.response || '';\n\nlet parsed;\ntry {\n  // Robustly extract the first JSON object from the response\n  // (handles cases where the model adds commentary before/after)\n  const jsonMatch = llmText.match(/\\{[\\s\\S]*\\}/);\n  if (!jsonMatch) throw new Error('No JSON object found in AI response');\n  parsed = JSON.parse(jsonMatch[0]);\n\n  // Validate required fields\n  const validCategories = ['Spam', 'Sales Lead', 'Tech Support', 'FAQ', 'Billing', 'Other'];\n  const validPriorities  = ['Low', 'Medium', 'High', 'Urgent'];\n  if (!validCategories.includes(parsed.category)) parsed.category = 'Other';\n  if (!validPriorities.includes(parsed.priority))  parsed.priority = 'Medium';\n\n} catch (e) {\n  // Graceful fallback \u2014 the draft will still be created for human review\n  parsed = {\n    category:    'Other',\n    priority:    'Medium',\n    summary:     'AI parsing failed \u2014 please review this email manually.',\n    sentiment:   'Neutral',\n    language:    'English',\n    draft_reply: 'Thank you for contacting us. A member of our team will review your message and respond as soon as possible.\\n\\nBest regards,\\n[YOUR NAME]'\n  };\n}\n\nreturn [{\n  json: {\n    // Original email fields\n    from:        emailData.from,\n    fromName:    emailData.fromName,\n    subject:     emailData.subject,\n    date:        emailData.date,\n    threadId:    emailData.threadId,\n    messageId:   emailData.messageId,\n    toAddress:   emailData.toAddress,\n    // AI triage results\n    category:    parsed.category,\n    priority:    parsed.priority,\n    summary:     parsed.summary    || '',\n    sentiment:   parsed.sentiment  || 'Neutral',\n    language:    parsed.language   || 'English',\n    draft_reply: parsed.draft_reply || '',\n    // Metadata\n    processedAt: new Date().toISOString(),\n    rawAiText:   llmText\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "5ac96dc1-08e2-425c-8be1-48ea52642055",
      "name": "Route by Category",
      "type": "n8n-nodes-base.switch",
      "position": [
        4272,
        1392
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Spam",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "Spam"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "8a154418-57ac-4929-91c6-d8665dbd8a66",
      "name": "Archive Spam (mark as read)",
      "type": "n8n-nodes-base.gmail",
      "position": [
        4576,
        1328
      ],
      "parameters": {
        "messageId": "={{ $json.messageId }}",
        "operation": "markAsRead"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "01fccce5-9506-4c4f-8a1d-1662b20cff6d",
      "name": "Create Draft Reply",
      "type": "n8n-nodes-base.gmail",
      "position": [
        4560,
        1536
      ],
      "parameters": {
        "message": "=\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\ud83e\udd16 AI TRIAGE \u2014 REVIEW BEFORE SENDING (delete this block)\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\nCategory : {{ $json.category }}\nPriority : {{ $json.priority }}\nSentiment: {{ $json.sentiment }}\nLanguage : {{ $json.language }}\nTo       : {{ $json.toAddress }}\nSummary  : {{ $json.summary }}\nProcessed: {{ $json.processedAt }}\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n{{ $json.draft_reply }}",
        "options": {
          "threadId": "={{ $json.threadId }}"
        },
        "subject": "=Re: {{ $json.subject }}",
        "resource": "draft"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "befb35b1-57e3-45a8-9091-ea331cb6c362",
      "name": "Sticky Note \u2014 Overview1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2864,
        1088
      ],
      "parameters": {
        "color": 7,
        "width": 796,
        "height": 600,
        "content": "## 1. Receive & Filter Email\n\nPolls Gmail every minute for new unread messages.\n\n**Filter node:** only lets through emails where the `from` field contains `support@` or `info@`. All other emails are silently ignored.\n\n> \u270f\ufe0f Update the Filter node values with your full addresses (e.g. `support@yourcompany.com`) for a stricter match."
      },
      "typeVersion": 1
    },
    {
      "id": "0d8caeae-92dd-4b78-84e3-8ebb1996496f",
      "name": "Sticky Note \u2014 Overview2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3664,
        1088
      ],
      "parameters": {
        "color": 7,
        "width": 556,
        "height": 680,
        "content": "## 2. AI Triage + Draft Reply\n### IONOS AI Model Hub (Llama 3.3-70B)\n\nSingle AI call returns structured JSON:\n- **category** (Spam / Sales Lead / Tech Support / FAQ / Billing / Other)\n- **priority** (Low / Medium / High / Urgent)\n- **summary** (one-sentence description)\n- **sentiment** (Positive / Neutral / Frustrated / Angry)\n- **language** (auto-detected)\n- **draft_reply** (complete reply, same language as email)"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Parse AI Response": {
      "main": [
        [
          {
            "node": "Route by Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Category": {
      "main": [
        [
          {
            "node": "Archive Spam (mark as read)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Draft Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Email Data": {
      "main": [
        [
          {
            "node": "AI Triage & Draft Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IONOS Cloud Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Triage & Draft Reply",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Only support@ or info@": {
      "main": [
        [
          {
            "node": "Prepare Email Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Triage & Draft Reply": {
      "main": [
        [
          {
            "node": "Parse AI Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New Email (info@ / support@)": {
      "main": [
        [
          {
            "node": "Only support@ or info@",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}