{
  "name": "AI Support Email Triage V2, Gmail Slack Jira",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyX",
              "value": 1,
              "unit": "minutes"
            }
          ]
        },
        "simple": false,
        "filters": {
          "labelIds": [
            "REPLACE_WITH_TEST_GMAIL_LABEL_ID"
          ],
          "q": "from:your-test-address@example.com to:your-test-address@example.com subject:\"[N8N TEST]\"",
          "readStatus": "unread"
        },
        "options": {}
      },
      "id": "8370e90e-6d1a-4404-afa7-a18b2a317347",
      "name": "Gmail Test Inbox Trigger",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.2,
      "position": [
        -1640,
        -240
      ],
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Gmail test account"
        }
      }
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "support-email-triage-v2",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "d09969e6-5309-4265-a30b-44a552c84278",
      "name": "Webhook Fallback",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -1640,
        0
      ],
      "webhookId": "3f6944f8-4b40-4665-8cf0-12e2e12cc9b4"
    },
    {
      "parameters": {},
      "id": "aaacf44a-950a-4c1d-9de5-6df510fefa17",
      "name": "Run Synthetic Test Set",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -1640,
        260
      ]
    },
    {
      "parameters": {
        "jsCode": "const emails = [\n  { id: 'V2-001', subject: 'Where is order 1042?', body: 'My tracking has not updated and the order was due yesterday.' },\n  { id: 'V2-002', subject: 'ClearAir 300 filter question', body: 'Which filter fits the ClearAir 300, and can I wash it?' },\n  { id: 'V2-003', subject: 'Wrong color BreezeMini', body: 'I received the wrong color and would like an exchange.' },\n  { id: 'V2-004', subject: 'Safety issue, purifier smoking', body: 'I am furious. The ClearAir 500 started smoking in my child\\'s room. This is dangerous. I need a manager today.' },\n  { id: 'V2-005', subject: 'Please refund my unopened fan', body: 'I changed my mind. The box is unopened. Please send return steps.' },\n  { id: 'V2-006', subject: 'BreezeTower display at night', body: 'How can I turn off the display? The fan works well.' },\n  { id: 'V2-007', subject: 'Card charge still pending', body: 'I canceled my order, but the card charge still shows as pending.' },\n  { id: 'V2-008', subject: 'Broken unit on arrival', body: 'The unit arrived broken. I am disappointed and need help.' },\n  { id: 'V2-009', subject: 'Guaranteed crypto profits', body: 'Click now for guaranteed crypto profits and backlinks.' },\n  { id: 'V2-010', subject: 'Noise level question', body: 'How quiet is the BreezeTower on its lowest setting?' },\n];\nreturn emails.map((email) => ({ json: { ...email, from: 'your-test-address@example.com', received_at: new Date().toISOString(), demo_mode: true, source: 'synthetic' } }));"
      },
      "id": "f573d6e3-1f4f-4fec-bc5e-5ed5e52ffe1f",
      "name": "Ten Synthetic Emails",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1420,
        260
      ]
    },
    {
      "parameters": {
        "jsCode": "const src = typeof $json.body === 'object' && $json.body !== null ? $json.body : $json;\nconst headers = src.payload?.headers ?? src.headers ?? [];\nconst header = (name) => headers.find((h) => String(h.name).toLowerCase() === name)?.value ?? '';\nconst subject = src.subject ?? header('subject');\nconst rawFrom = src.from ?? header('from');\nconst from = typeof rawFrom === 'object' && rawFrom !== null ? (rawFrom.text ?? rawFrom.value?.[0]?.address ?? '') : rawFrom;\nconst body = src.textPlain ?? src.text ?? src.snippet ?? src.body ?? '';\nconst required = { from, subject, body };\nconst missing = Object.entries(required).filter(([, value]) => !String(value ?? '').trim()).map(([key]) => key);\nif (missing.length) throw new Error('Missing required fields: ' + missing.join(', '));\nreturn { json: {\n  email_id: String(src.id ?? src.messageId ?? ('WEB-' + Date.now())),\n  thread_id: String(src.threadId ?? ''),\n  from: String(from).trim().slice(0, 320),\n  subject: String(subject).trim().slice(0, 500),\n  body: String(body).trim().slice(0, 12000),\n  email_link: src.webViewLink ?? (src.threadId ? 'https://mail.google.com/mail/u/0/#inbox/' + src.threadId : ''),\n  received_at: String(src.received_at ?? src.internalDate ?? new Date().toISOString()),\n  started_at_ms: Date.now(),\n  demo_mode: src.demo_mode === true || String(subject).startsWith('[N8N TEST]'),\n  source: src.source ?? ((src.labelIds || src.threadId || src.internalDate) ? 'gmail' : 'webhook')\n} };",
        "mode": "runOnceForEachItem"
      },
      "id": "e7633a39-0f27-46c9-b324-47cae1743c86",
      "name": "Normalize Email",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1160,
        0
      ]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "03ae1824-7651-466f-a7e7-2747122c282c",
      "name": "Loop Over Emails",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -1030,
        0
      ]
    },
    {
      "parameters": {
        "mode": "expression",
        "numberOutputs": 2,
        "output": "={{ $json.demo_mode === true ? 0 : 1 }}",
        "options": {}
      },
      "id": "914bc542-710b-4f30-85de-ef8b490231e1",
      "name": "AI Mode",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        -920,
        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": "b3fb167a-789c-4f7d-a028-fe9ba679d6dd",
      "name": "OpenAI Classify",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -680,
        120
      ],
      "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.' } } };",
        "mode": "runOnceForEachItem"
      },
      "id": "a98c65f1-4b1e-4f50-a159-fda9c5c9f920",
      "name": "Demo Classifier",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -680,
        -120
      ]
    },
    {
      "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 } };",
        "mode": "runOnceForEachItem"
      },
      "id": "fc3077f7-667b-46e5-86e4-53681a8952c0",
      "name": "Validate Classification",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -420,
        0
      ]
    },
    {
      "parameters": {
        "mode": "expression",
        "numberOutputs": 4,
        "output": "={{ ({ escalation: 0, draft: 1, archive: 2 }[$json.route] ?? 3) }}",
        "options": {}
      },
      "id": "c9279d41-ba0a-4012-8a3e-7fe0b30e358f",
      "name": "Route by Risk and Intent",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        -160,
        0
      ]
    },
    {
      "parameters": {
        "project": {
          "__rl": true,
          "value": "REPLACE_WITH_JIRA_PROJECT_ID",
          "mode": "list",
          "cachedResultName": "Support Triage Lab"
        },
        "issueType": {
          "__rl": true,
          "value": "REPLACE_WITH_JIRA_ISSUE_TYPE_ID",
          "mode": "list",
          "cachedResultName": "Task"
        },
        "summary": "=[HIGH] {{ $json.subject }}",
        "additionalFields": {
          "description": "=Synthetic support test.\\n\\nUrgency: {{ $json.classification.urgency }}\\nSentiment: {{ $json.classification.sentiment }}\\nReason: {{ $json.classification.reason }}\\nFrom: {{ $json.from }}\\nEmail: {{ $json.email_link || 'local test input' }}\\n\\nNo customer reply was sent."
        }
      },
      "id": "3319dc04-e2fa-4f07-9105-ac977fd0cf84",
      "name": "Create Jira Escalation",
      "type": "n8n-nodes-base.jira",
      "typeVersion": 1,
      "position": [
        100,
        -380
      ],
      "credentials": {
        "jiraSoftwareCloudApi": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Jira SW Cloud account"
        }
      }
    },
    {
      "parameters": {
        "mode": "expression",
        "numberOutputs": 2,
        "output": "={{ $json.demo_mode === true ? 0 : 1 }}",
        "options": {}
      },
      "id": "c8b3744f-c3f5-4cc3-b765-bdffb1971068",
      "name": "Draft Mode",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        100,
        -80
      ]
    },
    {
      "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": "0ddb8ce7-b27c-4a91-abc6-2e7dc6c437cd",
      "name": "OpenAI Draft Reply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        340,
        20
      ],
      "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 } };",
        "mode": "runOnceForEachItem"
      },
      "id": "e1c465bb-dfa7-466c-ba8b-53b76e4cca17",
      "name": "Demo FAQ Draft",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        340,
        -180
      ]
    },
    {
      "parameters": {
        "jsCode": "const source = $('Validate Classification').last().json;\nlet draft = $json.draft_reply ? $json : null;\nif (!draft) {\n  const raw = $json.output_text ?? $json.output?.[0]?.content?.[0]?.text;\n  if (!raw) throw new Error('OpenAI response did not contain draft text');\n  draft = JSON.parse(raw);\n}\nreturn { json: { ...source, draft_reply: String(draft.draft_reply), faq_ids: draft.faq_ids ?? [] } };",
        "mode": "runOnceForEachItem"
      },
      "id": "d196f028-3f2f-41b6-a3ed-33dcaa931cf6",
      "name": "Prepare Slack Approval",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        580,
        -80
      ]
    },
    {
      "parameters": {
        "authentication": "accessToken",
        "operation": "sendAndWait",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "REPLACE_WITH_SLACK_CHANNEL_ID",
          "mode": "id"
        },
        "message": "=*Support reply needs approval*\\n\\n*Subject:* {{ $json.subject }}\\n*Intent:* {{ $json.classification.intent }}\\n*Urgency:* {{ $json.classification.urgency }}\\n*Sentiment:* {{ $json.classification.sentiment }}\\n*Confidence:* {{ $json.classification.confidence }}\\n\\n*Draft*\\n{{ $json.draft_reply }}\\n\\nNothing is sent to the test customer until you approve.",
        "approvalOptions": {
          "values": {
            "approvalType": "double",
            "approveLabel": "Approve",
            "disapproveLabel": "Reject"
          }
        },
        "options": {
          "appendAttribution": false
        }
      },
      "id": "f4bd6b72-96c6-4a82-9311-12b25f602f64",
      "name": "Slack Approve or Reject",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        820,
        -80
      ],
      "webhookId": "cb3d8cd3-12dc-41d3-a610-699e729512b0",
      "credentials": {
        "slackApi": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Slack triage workspace"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const source = $('Prepare Slack Approval').last().json;\nreturn { json: {\n  timestamp: new Date().toISOString(),\n  message_id: source.email_id,\n  action_type: 'slack_approval_response',\n  target_system: 'Slack',\n  status: $json.data?.approved === true ? 'approved' : 'rejected',\n  external_id: $json.ts ?? $json.message_timestamp ?? '',\n  actor: 'Slack approver',\n  reason: $json.data?.approved === true ? 'Approved in #triage-approvals' : 'Rejected in #triage-approvals',\n  request_json: JSON.stringify({ channel: '#triage-approvals', subject: source.subject, draft_reply: source.draft_reply }),\n  response_json: JSON.stringify($json),\n  latency_ms: Date.now() - source.started_at_ms,\n  execution_id: $execution.id\n} };",
        "mode": "runOnceForEachItem"
      },
      "id": "5bca0ef7-db30-4537-99d7-a226e89355de",
      "name": "Build Slack Approval Audit",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1060,
        -360
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_GOOGLE_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "actions",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "6642efcb-1b36-4ac6-b6ef-711788c57486",
      "name": "Append Slack Approval Audit",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.6,
      "position": [
        1300,
        -360
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Google Sheets audit log"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "c374d4a9-a038-403b-a99d-3dd423ff798f",
              "leftValue": "={{ $json.data.approved }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5f4b1cfb-3385-4dc1-b428-41df3d9c26b7",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1060,
        -80
      ]
    },
    {
      "parameters": {
        "operation": "reply",
        "messageId": "={{ $('Prepare Slack Approval').last().json.email_id }}",
        "emailType": "text",
        "message": "={{ $('Prepare Slack Approval').last().json.draft_reply }}",
        "options": {
          "appendAttribution": false,
          "replyToSenderOnly": true,
          "senderName": "Northstar Home Test Support"
        }
      },
      "id": "f814bf1c-482f-47b5-950a-b565d60168b0",
      "name": "Send Approved Gmail Reply",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1300,
        -180
      ],
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Gmail test account"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "return { json: {\n  ...$json,\n  data: {\n    ...($json.data ?? {}),\n    approved: false,\n    text: 'Draft needs edits. Rejected in the Slack approval card.'\n  }\n} };"
      },
      "id": "748e7f22-07dd-4f4d-981c-d108446bc98b",
      "name": "Record Rejection Reason",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1300,
        20
      ]
    },
    {
      "parameters": {
        "authentication": "accessToken",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "REPLACE_WITH_SLACK_CHANNEL_ID",
          "mode": "id"
        },
        "text": "=*Agent review queued*\\n{{ $json.subject }}\\nIntent: {{ $json.classification.intent }}\\nUrgency: {{ $json.classification.urgency }}\\nNo customer reply was sent.",
        "otherOptions": {
          "includeLinkToWorkflow": true
        }
      },
      "id": "de80a0a7-3f5c-4892-8fd8-46426f6882d2",
      "name": "Post Agent Review Notice",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        100,
        340
      ],
      "credentials": {
        "slackApi": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Slack triage workspace"
        }
      }
    },
    {
      "parameters": {
        "operation": "markAsRead",
        "messageId": "={{ $json.email_id }}"
      },
      "id": "f6bac54a-c843-4260-baa6-ba91a6c0140a",
      "name": "Mark Test Spam Read",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        100,
        160
      ],
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Gmail test account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const source = $('Validate Classification').last().json;\nconst now = new Date().toISOString();\nlet action_type = 'jira_issue_created';\nlet target_system = 'Jira';\nlet status = 'created';\nlet external_id = $json.key ?? $json.id ?? '';\nlet actor = 'n8n';\nlet reason = source.classification.reason ?? '';\nlet request = { summary: '[HIGH] ' + source.subject, email_link: source.email_link, classification: source.classification };\nlet response = $json;\nif (source.route === 'draft') {\n  const rejected = $json.data?.approved === false || Boolean($json.data?.text);\n  action_type = rejected ? 'reply_rejected' : 'gmail_reply_sent';\n  target_system = rejected ? 'Slack' : 'Gmail';\n  status = rejected ? 'rejected' : 'sent';\n  external_id = rejected ? source.email_id : ($json.id ?? source.email_id);\n  actor = 'Slack approver';\n  reason = rejected ? ($json.data?.text ?? 'Draft needs edits.') : 'Approved in #triage-approvals';\n  request = { message_id: source.email_id, draft_reply: $('Prepare Slack Approval').last().json.draft_reply };\n  response = $json;\n} else if (source.route === 'agent-review') {\n  action_type = 'agent_review_notice'; target_system = 'Slack'; status = 'posted'; external_id = $json.ts ?? $json.message_timestamp ?? '';\n  request = { subject: source.subject, classification: source.classification };\n} else if (source.route === 'archive') {\n  action_type = 'spam_marked_read'; target_system = 'Gmail'; status = 'done'; external_id = source.email_id;\n  request = { message_id: source.email_id };\n}\nreturn { json: { timestamp: now, message_id: source.email_id, action_type, target_system, status, external_id, actor, reason,\n  request_json: JSON.stringify(request), response_json: JSON.stringify(response), latency_ms: Date.now() - source.started_at_ms, execution_id: $execution.id } };",
        "mode": "runOnceForEachItem"
      },
      "id": "2177e882-cb05-4bd4-83ae-7e7774e3cdd9",
      "name": "Build Action Log Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1580,
        80
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_GOOGLE_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "actions",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "5911cc9f-de7d-44f7-98bb-175050d99889",
      "name": "Append Action Log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.6,
      "position": [
        1820,
        80
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Google Sheets audit log"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const source = $('Validate Classification').last().json;\nconst c = source.classification ?? {};\nconst action = $('Build Action Log Row').last().json;\nconst approved = action.action_type === 'gmail_reply_sent';\nreturn { json: {\n  timestamp: new Date().toISOString(), email_id: source.email_id, from: source.from, subject: source.subject,\n  intent: c.intent ?? '', urgency: c.urgency ?? '', sentiment: c.sentiment ?? '', confidence: c.confidence ?? '',\n  action_taken: action.action_type, approved, reviewer: action.actor, demo_mode: source.demo_mode === true,\n  classification_json: JSON.stringify(c), route_taken: source.route, approver_action: approved ? 'approved' : (action.status === 'rejected' ? 'rejected: ' + action.reason : 'not_applicable'),\n  latency_ms: Date.now() - source.started_at_ms\n} };",
        "mode": "runOnceForEachItem"
      },
      "id": "7dac247b-cc07-4e21-8bbf-a997a6af03dc",
      "name": "Build Processed Email Log Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2060,
        80
      ]
    },
    {
      "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": "f92413fb-0198-49c2-b9a8-3562cea435c9",
      "name": "Append Processed Email Log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.6,
      "position": [
        2300,
        80
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_WITH_N8N_CREDENTIAL_ID",
          "name": "Google Sheets audit log"
        }
      }
    },
    {
      "parameters": {
        "content": "## V2 setup\\nGmail: label `n8n-triage-test`, query restricted to Peter sending to himself with `[N8N TEST]` in the subject.\\n\\nSlack: `#triage-approvals`, app must be in the channel.\\n\\nJira: free test project and Task issue type.\\n\\nEvery side effect is written to `actions`. Every email is written to `Triage Log`. Credentials stay in n8n.",
        "height": 300,
        "width": 520,
        "color": 5
      },
      "id": "898ae62e-ba1e-46d6-b4cf-ee9e45720c89",
      "name": "V2 Setup Notes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1640,
        -620
      ]
    }
  ],
  "connections": {
    "Gmail Test Inbox Trigger": {
      "main": [
        [
          {
            "node": "Normalize Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Fallback": {
      "main": [
        [
          {
            "node": "Normalize Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Synthetic Test Set": {
      "main": [
        [
          {
            "node": "Ten Synthetic Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ten Synthetic Emails": {
      "main": [
        [
          {
            "node": "Normalize Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Email": {
      "main": [
        [
          {
            "node": "Loop Over Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Emails": {
      "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": "Create Jira Escalation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Draft Mode",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Test Spam Read",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post Agent Review Notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Jira Escalation": {
      "main": [
        [
          {
            "node": "Build Action 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 Slack Approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Demo FAQ Draft": {
      "main": [
        [
          {
            "node": "Prepare Slack Approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Slack Approval": {
      "main": [
        [
          {
            "node": "Slack Approve or Reject",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack Approve or Reject": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Slack Approval Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Slack Approval Audit": {
      "main": [
        [
          {
            "node": "Append Slack Approval Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved?": {
      "main": [
        [
          {
            "node": "Send Approved Gmail Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Record Rejection Reason",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Approved Gmail Reply": {
      "main": [
        [
          {
            "node": "Build Action Log Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Record Rejection Reason": {
      "main": [
        [
          {
            "node": "Build Action Log Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Agent Review Notice": {
      "main": [
        [
          {
            "node": "Build Action Log Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Test Spam Read": {
      "main": [
        [
          {
            "node": "Build Action Log Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Action Log Row": {
      "main": [
        [
          {
            "node": "Append Action Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Action Log": {
      "main": [
        [
          {
            "node": "Build Processed Email Log Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Processed Email Log Row": {
      "main": [
        [
          {
            "node": "Append Processed Email Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Processed Email Log": {
      "main": [
        [
          {
            "node": "Loop Over Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
