{
  "id": "wf-support-email-triage",
  "name": "AI Support Email Triage with Human Approval",
  "nodes": [
    {
      "parameters": {"inputSource": "passthrough"},
      "id": "00-cli-trigger",
      "name": "Run Synthetic Test",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [-1380, 220]
    },
    {
      "parameters": {
        "jsCode": "return [{ json: { id: 'SUP-1004', from: 'darren.brooks@example.com', subject: 'Your purifier smoked and nearly caused a fire', body: `I am furious. The ClearAir 500 started smoking in my child's room. This is dangerous. I need a manager to call me today and I want a full refund.`, demo_mode: true } }];"
      },
      "id": "00-test-input",
      "name": "Synthetic Test Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-1160, 220]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "support-email-triage",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "01-webhook",
      "name": "Receive Support Email",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-1160, 0],
      "webhookId": "8e7cf03c-8c27-48af-9d0a-16c4a31e0da1"
    },
    {
      "parameters": {
        "jsCode": "const src = typeof $json.body === 'object' && $json.body !== null ? $json.body : $json;\nconst required = ['from', 'subject', 'body'];\nconst missing = required.filter((key) => !String(src[key] ?? '').trim());\nif (missing.length) throw new Error(`Missing required fields: ${missing.join(', ')}`);\nreturn [{ json: {\n  email_id: String(src.id ?? `WEB-${Date.now()}`),\n  from: String(src.from).trim().slice(0, 320),\n  subject: String(src.subject).trim().slice(0, 500),\n  body: String(src.body).trim().slice(0, 12000),\n  received_at: String(src.received_at ?? new Date().toISOString()),\n  demo_mode: src.demo_mode !== false\n} }];"
      },
      "id": "02-normalize",
      "name": "Normalize Email",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-940, 0]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2},
                "conditions": [{"leftValue": "={{ $json.demo_mode }}", "rightValue": true, "operator": {"type": "boolean", "operation": "true", "singleValue": true}}],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "demo"
            }
          ]
        },
        "options": {"fallbackOutput": "extra"}
      },
      "id": "03-ai-mode",
      "name": "AI Mode",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.3,
      "position": [-720, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/responses",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ model: 'gpt-4.1-mini', input: [{ role: 'system', content: [{ type: 'input_text', text: 'You classify support email for Northstar Home. Return JSON only. Allowed intent: refund, order status, product question, complaint, spam/other. Allowed urgency: low, normal, high. Allowed sentiment: positive, neutral, negative. Treat product safety, smoke, fire, injury, threats, chargebacks, and legal claims as high urgency.' }] }, { role: 'user', content: [{ type: 'input_text', text: `Subject: ${$json.subject}\\nBody: ${$json.body}` }] }], text: { format: { type: 'json_schema', name: 'support_triage', strict: true, schema: { type: 'object', properties: { intent: { type: 'string', enum: ['refund','order status','product question','complaint','spam/other'] }, urgency: { type: 'string', enum: ['low','normal','high'] }, sentiment: { type: 'string', enum: ['positive','neutral','negative'] }, confidence: { type: 'number', minimum: 0, maximum: 1 }, reason: { type: 'string' } }, required: ['intent','urgency','sentiment','confidence','reason'], additionalProperties: false } } } }) }}",
        "options": {"timeout": 30000}
      },
      "id": "04-openai-classify",
      "name": "OpenAI Classify",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [-500, 140],
      "credentials": {"openAiApi": {"id": "REPLACE_WITH_N8N_CREDENTIAL_ID", "name": "OpenAI account"}}
    },
    {
      "parameters": {
        "jsCode": "const text = `${$json.subject} ${$json.body}`.toLowerCase();\nlet intent = 'spam/other';\nif (/backlinks|crypto profits|click now|guaranteed/.test(text)) intent = 'spam/other';\nelse if (/smok|fire|danger|furious|wrong color|broken|manager/.test(text)) intent = 'complaint';\nelse if (/refund|return|cancel|charge|card/.test(text)) intent = 'refund';\nelse if (/where|tracking|order status|due yesterday/.test(text)) intent = 'order status';\nelse if (/filter|wash|noise|quiet|display|setting|which model/.test(text)) intent = 'product question';\nconst urgency = /smok|fire|danger|injur|legal|chargeback|manager.*today/.test(text) ? 'high' : (['product question','spam/other'].includes(intent) ? 'low' : 'normal');\nconst sentiment = /furious|danger|disappoint|wrong|broken|backlinks|crypto/.test(text) ? 'negative' : (/thank|works well/.test(text) ? 'positive' : 'neutral');\nreturn [{ json: { ...$json, classification: { intent, urgency, sentiment, confidence: 0.93, reason: 'Fixed synthetic-data rule for the no-key demo.' } } }];"
      },
      "id": "05-demo-classify",
      "name": "Demo Classifier",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-500, -140]
    },
    {
      "parameters": {
        "jsCode": "const source = $('Normalize Email').item.json;\nlet parsed = $json.classification;\nif (!parsed) {\n  const raw = $json.output_text ?? $json.output?.[0]?.content?.[0]?.text ?? $json.choices?.[0]?.message?.content;\n  if (!raw) throw new Error('OpenAI response did not contain classification text');\n  parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;\n}\nconst allowed = { intent: ['refund','order status','product question','complaint','spam/other'], urgency: ['low','normal','high'], sentiment: ['positive','neutral','negative'] };\nfor (const key of Object.keys(allowed)) if (!allowed[key].includes(parsed[key])) throw new Error(`Invalid ${key}: ${parsed[key]}`);\nconst route = parsed.intent === 'complaint' && parsed.urgency === 'high' ? 'escalation' : ['order status','product question'].includes(parsed.intent) ? 'draft' : parsed.intent === 'spam/other' ? 'archive' : 'agent-review';\nreturn [{ json: { ...source, classification: { ...parsed, confidence: Math.max(0, Math.min(1, Number(parsed.confidence ?? 0))) }, route } }];"
      },
      "id": "06-validate",
      "name": "Validate Classification",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-240, 0]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {"conditions": {"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2}, "conditions": [{"leftValue": "={{ $json.route }}", "rightValue": "escalation", "operator": {"type": "string", "operation": "equals"}}], "combinator": "and"}, "renameOutput": true, "outputKey": "escalation"},
            {"conditions": {"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2}, "conditions": [{"leftValue": "={{ $json.route }}", "rightValue": "draft", "operator": {"type": "string", "operation": "equals"}}], "combinator": "and"}, "renameOutput": true, "outputKey": "draft"},
            {"conditions": {"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2}, "conditions": [{"leftValue": "={{ $json.route }}", "rightValue": "archive", "operator": {"type": "string", "operation": "equals"}}], "combinator": "and"}, "renameOutput": true, "outputKey": "archive"}
          ]
        },
        "options": {"fallbackOutput": "extra"}
      },
      "id": "07-route",
      "name": "Route by Risk and Intent",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.3,
      "position": [20, 0]
    },
    {
      "parameters": {
        "assignments": {"assignments": [{"id": "a1", "name": "action_taken", "value": "escalated to supervisor", "type": "string"}, {"id": "a2", "name": "approved", "value": "n/a", "type": "string"}, {"id": "a3", "name": "queue", "value": "P1 safety and complaint", "type": "string"}]},
        "includeOtherFields": true,
        "options": {}
      },
      "id": "08-escalate",
      "name": "Escalate to Supervisor",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [300, -300]
    },
    {
      "parameters": {
        "rules": {"values": [{"conditions": {"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2}, "conditions": [{"leftValue": "={{ $json.demo_mode }}", "rightValue": true, "operator": {"type": "boolean", "operation": "true", "singleValue": true}}], "combinator": "and"}, "renameOutput": true, "outputKey": "demo"}]},
        "options": {"fallbackOutput": "extra"}
      },
      "id": "09-draft-mode",
      "name": "Draft Mode",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.3,
      "position": [300, -60]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/responses",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ model: 'gpt-4.1-mini', input: [{ role: 'system', content: [{ type: 'input_text', text: `Draft a short support reply for Northstar Home. Use only these facts. Do not promise stock, dates, refunds, or discounts beyond the facts. Return JSON with draft_reply and faq_ids.\\nFAQ-01: Unopened products can be returned within 30 days after delivery. A return must be approved before shipping.\\nFAQ-02: Standard shipping is 3 to 5 business days after dispatch. Tracking may take 24 hours to update.\\nFAQ-03: ClearAir 300 uses filter CA-300. Replace it every 6 to 8 months. Do not wash it.\\nFAQ-04: ClearAir 500 uses filter CA-500. Stop using any unit that smells hot, sparks, or smokes, unplug it, and contact support.\\nFAQ-05: BreezeTower runs at 28 dB on low. Its display can be turned off by holding Timer for 3 seconds.\\nFAQ-06: BreezeMini has a 2-year limited warranty for faults. Color exchanges follow the 30-day return process.\\nFAQ-07: Card authorizations can remain pending for 3 to 7 business days after cancellation.` }] }, { role: 'user', content: [{ type: 'input_text', text: `Subject: ${$json.subject}\\nBody: ${$json.body}` }] }], text: { format: { type: 'json_schema', name: 'support_draft', strict: true, schema: { type: 'object', properties: { draft_reply: { type: 'string' }, faq_ids: { type: 'array', items: { type: 'string' } } }, required: ['draft_reply','faq_ids'], additionalProperties: false } } } }) }}",
        "options": {"timeout": 30000}
      },
      "id": "10-openai-draft",
      "name": "OpenAI Draft Reply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [540, 40],
      "credentials": {"openAiApi": {"id": "REPLACE_WITH_N8N_CREDENTIAL_ID", "name": "OpenAI account"}}
    },
    {
      "parameters": {
        "jsCode": "const c = $json.classification;\nlet draft_reply; let faq_ids;\nif (c.intent === 'order status') { draft_reply = `Hi, thanks for checking on your order. Standard shipping is 3 to 5 business days after dispatch, and tracking can take up to 24 hours to update. A support agent can check ${$json.email_id} before this reply is sent.`; faq_ids = ['FAQ-02']; }\nelse { draft_reply = `Hi, thanks for your question. The ClearAir 300 uses filter CA-300. It should be replaced every 6 to 8 months and should not be washed. Please confirm your model before buying a filter.`; faq_ids = ['FAQ-03']; }\nreturn [{ json: { ...$json, draft_reply, faq_ids } }];"
      },
      "id": "11-demo-draft",
      "name": "Demo FAQ Draft",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [540, -160]
    },
    {
      "parameters": {
        "jsCode": "const source = $('Validate Classification').item.json;\nlet draft = $json.draft_reply ? $json : null;\nif (!draft) { const raw = $json.output_text ?? $json.output?.[0]?.content?.[0]?.text; if (!raw) throw new Error('OpenAI response did not contain draft text'); draft = JSON.parse(raw); }\nreturn [{ json: { ...source, draft_reply: String(draft.draft_reply), faq_ids: draft.faq_ids ?? [], action_taken: 'draft awaiting approval', approved: 'pending', approval_note: 'No customer message is sent by this prototype.' } }];"
      },
      "id": "12-prepare-approval",
      "name": "Prepare Approval",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [780, -60]
    },
    {
      "parameters": {"resume": "webhook", "httpMethod": "POST", "options": {"webhookSuffix": "support-draft-approval"}},
      "id": "13-wait",
      "name": "Wait for Human Approval",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [1020, -60],
      "webhookId": "333e8d91-ccf6-4017-9219-bfc9186d8f21"
    },
    {
      "parameters": {
        "conditions": {"options": {"caseSensitive": false, "leftValue": "", "typeValidation": "strict", "version": 2}, "conditions": [{"id": "approve-check", "leftValue": "={{ $json.body?.decision ?? $json.decision ?? '' }}", "rightValue": "approve", "operator": {"type": "string", "operation": "equals"}}], "combinator": "and"},
        "options": {}
      },
      "id": "14-approved",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1260, -60]
    },
    {
      "parameters": {"assignments": {"assignments": [{"id": "ok1", "name": "action_taken", "value": "draft approved", "type": "string"}, {"id": "ok2", "name": "approved", "value": true, "type": "boolean"}]}, "includeOtherFields": true, "options": {}},
      "id": "15-approved-set",
      "name": "Record Approval",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1490, -140]
    },
    {
      "parameters": {"assignments": {"assignments": [{"id": "no1", "name": "action_taken", "value": "draft rejected", "type": "string"}, {"id": "no2", "name": "approved", "value": false, "type": "boolean"}]}, "includeOtherFields": true, "options": {}},
      "id": "16-rejected-set",
      "name": "Record Rejection",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1490, 20]
    },
    {
      "parameters": {"assignments": {"assignments": [{"id": "sp1", "name": "action_taken", "value": "archived", "type": "string"}, {"id": "sp2", "name": "approved", "value": "n/a", "type": "string"}]}, "includeOtherFields": true, "options": {}},
      "id": "17-archive",
      "name": "Archive Spam",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [300, 170]
    },
    {
      "parameters": {"assignments": {"assignments": [{"id": "ar1", "name": "action_taken", "value": "agent review", "type": "string"}, {"id": "ar2", "name": "approved", "value": "n/a", "type": "string"}]}, "includeOtherFields": true, "options": {}},
      "id": "18-review",
      "name": "Queue for Agent Review",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [300, 360]
    },
    {
      "parameters": {
        "jsCode": "const email = $('Normalize Email').first().json;\nconst classified = $('Validate Classification').first().json;\nconst c = classified.classification ?? {};\n\nreturn [{\n  json: {\n    timestamp: new Date().toISOString(),\n    email_id: email.email_id ?? '',\n    from: email.from ?? '',\n    subject: email.subject ?? '',\n    intent: c.intent ?? '',\n    urgency: c.urgency ?? '',\n    sentiment: c.sentiment ?? '',\n    confidence: c.confidence ?? '',\n    action_taken: $json.action_taken ?? '',\n    approved: $json.approved ?? false,\n    reviewer: $json.body?.reviewer ?? $json.reviewer ?? '',\n    demo_mode: email.demo_mode === true,\n  },\n}];"
      },
      "id": "19-build-log",
      "name": "Build Log Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1740, 220]
    },
    {
      "parameters": {
        "rules": {"values": [{"conditions": {"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2}, "conditions": [{"leftValue": "={{ $json.demo_mode }}", "rightValue": true, "operator": {"type": "boolean", "operation": "true", "singleValue": true}}], "combinator": "and"}, "renameOutput": true, "outputKey": "sample CSV"}]},
        "options": {"fallbackOutput": "extra"}
      },
      "id": "20-log-mode",
      "name": "Log Mode",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.3,
      "position": [1960, 220]
    },
    {
      "parameters": {"assignments": {"assignments": [{"id": "dl1", "name": "log_status", "value": "matched sample CSV schema", "type": "string"}]}, "includeOtherFields": true, "options": {}},
      "id": "21-demo-log",
      "name": "Demo Log Complete",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [2190, 120]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {"__rl": true, "value": "REPLACE_WITH_GOOGLE_SHEET_ID", "mode": "id"},
        "sheetName": {"__rl": true, "value": "Triage Log", "mode": "name"},
        "columns": {"mappingMode": "autoMapInputData", "value": {}, "matchingColumns": [], "schema": [], "attemptToConvertTypes": false, "convertFieldsToString": false},
        "options": {}
      },
      "id": "22-sheets",
      "name": "Append Log Row",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.6,
      "position": [2190, 300],
      "credentials": {"googleSheetsOAuth2Api": {"id": "REPLACE_WITH_N8N_CREDENTIAL_ID", "name": "Google Sheets account"}}
    }
  ],
  "pinData": {
    "Receive Support Email": [{"json": {"body": {"id": "SUP-1004", "from": "darren.brooks@example.com", "subject": "Your purifier smoked and nearly caused a fire", "body": "I am furious. The ClearAir 500 started smoking in my child's room. This is dangerous. I need a manager to call me today and I want a full refund.", "demo_mode": true}}}],
    "Validate Classification": [{"json": {"email_id": "SUP-1004", "from": "darren.brooks@example.com", "subject": "Your purifier smoked and nearly caused a fire", "body": "I am furious. The ClearAir 500 started smoking in my child's room. This is dangerous. I need a manager to call me today and I want a full refund.", "received_at": "2026-07-16T09:09:41-04:00", "demo_mode": true, "classification": {"intent": "complaint", "urgency": "high", "sentiment": "negative", "confidence": 0.97, "reason": "Safety complaint mentions smoke, danger, and a same-day manager request."}, "route": "escalation"}}],
    "Prepare Approval": [{"json": {"email_id": "SUP-1002", "from": "omar.rivera@example.com", "subject": "Air purifier filter replacement", "classification": {"intent": "product question", "urgency": "low", "sentiment": "neutral", "confidence": 0.95, "reason": "Customer asks for product care facts."}, "draft_reply": "Hi Omar, the ClearAir 300 uses filter CA-300. Replace it every 6 to 8 months, and do not wash it. Please confirm the model number before ordering.", "faq_ids": ["FAQ-03"], "action_taken": "draft awaiting approval", "approved": "pending"}}]
  },
  "connections": {
    "Run Synthetic Test": {"main": [[{"node": "Synthetic Test Input", "type": "main", "index": 0}]]},
    "Synthetic Test Input": {"main": [[{"node": "Normalize Email", "type": "main", "index": 0}]]},
    "Receive Support Email": {"main": [[{"node": "Normalize Email", "type": "main", "index": 0}]]},
    "Normalize Email": {"main": [[{"node": "AI Mode", "type": "main", "index": 0}]]},
    "AI Mode": {"main": [[{"node": "Demo Classifier", "type": "main", "index": 0}], [{"node": "OpenAI Classify", "type": "main", "index": 0}]]},
    "OpenAI Classify": {"main": [[{"node": "Validate Classification", "type": "main", "index": 0}]]},
    "Demo Classifier": {"main": [[{"node": "Validate Classification", "type": "main", "index": 0}]]},
    "Validate Classification": {"main": [[{"node": "Route by Risk and Intent", "type": "main", "index": 0}]]},
    "Route by Risk and Intent": {"main": [[{"node": "Escalate to Supervisor", "type": "main", "index": 0}], [{"node": "Draft Mode", "type": "main", "index": 0}], [{"node": "Archive Spam", "type": "main", "index": 0}], [{"node": "Queue for Agent Review", "type": "main", "index": 0}]]},
    "Escalate to Supervisor": {"main": [[{"node": "Build Log Row", "type": "main", "index": 0}]]},
    "Draft Mode": {"main": [[{"node": "Demo FAQ Draft", "type": "main", "index": 0}], [{"node": "OpenAI Draft Reply", "type": "main", "index": 0}]]},
    "OpenAI Draft Reply": {"main": [[{"node": "Prepare Approval", "type": "main", "index": 0}]]},
    "Demo FAQ Draft": {"main": [[{"node": "Prepare Approval", "type": "main", "index": 0}]]},
    "Prepare Approval": {"main": [[{"node": "Wait for Human Approval", "type": "main", "index": 0}]]},
    "Wait for Human Approval": {"main": [[{"node": "Approved?", "type": "main", "index": 0}]]},
    "Approved?": {"main": [[{"node": "Record Approval", "type": "main", "index": 0}], [{"node": "Record Rejection", "type": "main", "index": 0}]]},
    "Record Approval": {"main": [[{"node": "Build Log Row", "type": "main", "index": 0}]]},
    "Record Rejection": {"main": [[{"node": "Build Log Row", "type": "main", "index": 0}]]},
    "Archive Spam": {"main": [[{"node": "Build Log Row", "type": "main", "index": 0}]]},
    "Queue for Agent Review": {"main": [[{"node": "Build Log Row", "type": "main", "index": 0}]]},
    "Build Log Row": {"main": [[{"node": "Log Mode", "type": "main", "index": 0}]]},
    "Log Mode": {"main": [[{"node": "Demo Log Complete", "type": "main", "index": 0}], [{"node": "Append Log Row", "type": "main", "index": 0}]]}
  },
  "active": false,
  "settings": {"executionOrder": "v1", "saveManualExecutions": true, "saveDataErrorExecution": "all", "saveDataSuccessExecution": "all"},
  "versionId": "a1fd65c1-0ecb-4b4d-9fd6-c913c3ce74aa",
  "meta": {"templateCredsSetupCompleted": false},
  "tags": []
}
