AutomationFlowsEmail & Gmail › Triage Customer Support Emails and Draft Gmail Replies with Ionos AI Model Hub

Triage Customer Support Emails and Draft Gmail Replies with Ionos AI Model Hub

ByFabrice @fwurtz on n8n.io

This n8n template shows you how to automate customer support email triage with a sovereign AI. By combining Gmail for inbox monitoring and the IONOS AI Model Hub, every customer email is classified and replied to by AI. Support inbox management: Automatically sort incoming…

Event trigger★★★★☆ complexityAI-powered12 nodesGmail TriggerChain Llm@Ionos Cloud/N8N Nodes Ionos CloudGmail
Email & Gmail Trigger: Event Nodes: 12 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #14966 — we link there as the canonical source.

This workflow follows the Chainllm → 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 →

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

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This n8n template shows you how to automate customer support email triage with a sovereign AI. By combining Gmail for inbox monitoring and the IONOS AI Model Hub, every customer email is classified and replied to by AI. Support inbox management: Automatically sort incoming…

Source: https://n8n.io/workflows/14966/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

NTF 03: Email Triage Agent. Uses gmailTrigger, lmChatAnthropic, chainLlm, gmail. Event-driven trigger; 9 nodes.

Gmail Trigger, Anthropic Chat, Chain Llm +2
Email & Gmail

Professionals and individuals who receive high volumes of emails, those who want to automatically organize their Gmail inbox using AI classification.

OpenRouter Chat, Chain Llm, Gmail Trigger +1
Email & Gmail

This workflow is for contractors, freelancers, local service businesses, and small teams that receive leads and customer requests through Gmail but do not have a dedicated sales or admin team.

Redis Trigger, Postgres, HTTP Request +5
Email & Gmail

This n8n template uses AI to automatically respond to your Gmail inbox by drafting response for your approval via email.

Gmail Trigger, Gmail, OpenAI Chat +1
Email & Gmail

This workflow contains community nodes that are only compatible with the self-hosted version of n8n.

OpenRouter Chat, Gmail, Gmail Trigger +2