{
  "name": "AI inbox triage (LLM + validation + fallback)",
  "nodes": [
    {
      "id": "1",
      "name": "Inbound message",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "triage",
        "responseMode": "responseNode",
        "options": {}
      },
      "webhookId": "5b3c2f6e-7d0a-4c2e-9a51-triage000001"
    },
    {
      "id": "2",
      "name": "Normalize + dedupe key",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        300
      ],
      "onError": "continueErrorOutput",
      "parameters": {
        "jsCode": "const b = $input.first().json.body || {};\nconst text = `${b.subject || ''}\\n${b.body || ''}`.trim();\nif (!text) throw new Error('empty message: need subject or body');\nconst fnv = (str) => { let h = 0x811c9dc5; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return h.toString(16).padStart(8, '0'); };\nreturn [{ json: { from: String(b.from || 'unknown').toLowerCase(), subject: b.subject || '', body: String(b.body || '').slice(0, 4000), text, dedupeKey: fnv(text.toLowerCase()), receivedAt: new Date().toISOString() } }];"
      }
    },
    {
      "id": "3",
      "name": "LLM classify",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        300
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput",
      "parameters": {
        "method": "POST",
        "url": "http://127.0.0.1:3457/v1/chat/completions",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'auto', temperature: 0, response_format: { type: 'json_object' }, messages: [ { role: 'system', content: \"You triage inbound business messages. Return ONLY JSON: {\\\"category\\\": one of [sales_lead, support, billing, partnership, spam, other], \\\"urgency\\\": one of [high, normal, low], \\\"summary\\\": string under 25 words, \\\"needs_reply\\\": boolean, \\\"confidence\\\": number 0-1}\" }, { role: 'user', content: $json.text } ] }) }}",
        "options": {
          "timeout": 45000
        }
      }
    },
    {
      "id": "4",
      "name": "Validate model output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        200
      ],
      "parameters": {
        "jsCode": "const src = $('Normalize + dedupe key').first().json;\nconst raw = $input.first().json?.choices?.[0]?.message?.content || '';\nlet p = null;\nconst m = raw.match(/\\{[\\s\\S]*\\}/);\ntry { p = JSON.parse(m ? m[0] : raw); } catch {}\nconst CATS = ['sales_lead','support','billing','partnership','spam','other'], URG = ['high','normal','low'];\nconst valid = p && CATS.includes(p.category) && URG.includes(p.urgency) && typeof p.summary === 'string' && typeof p.needs_reply === 'boolean';\nif (!valid) return [{ json: { ...src, category: 'other', urgency: 'normal', summary: 'Model output failed validation; sent to a human.', needs_reply: true, confidence: 0, source: 'needs_human', route: 'urgent', raw: raw.slice(0, 300) } }];\nconst route = p.category === 'spam' ? 'drop' : (p.urgency === 'high' || (p.confidence ?? 1) < 0.6) ? 'urgent' : 'queue';\nreturn [{ json: { ...src, ...p, source: 'llm', route } }];"
      }
    },
    {
      "id": "5",
      "name": "Fallback rules (LLM down)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        420
      ],
      "parameters": {
        "jsCode": "const src = $('Normalize + dedupe key').first().json;\nconst t = src.text.toLowerCase();\nconst category = /invoice|refund|charge|payment/.test(t) ? 'billing' : /price|quote|demo|buy|interested/.test(t) ? 'sales_lead' : /broken|error|help|issue|not working/.test(t) ? 'support' : 'other';\nconst urgency = /urgent|asap|today|down|immediately/.test(t) ? 'high' : 'normal';\nreturn [{ json: { ...src, category, urgency, summary: src.subject || src.text.slice(0, 80), needs_reply: true, confidence: 0.4, source: 'fallback_rules', route: urgency === 'high' ? 'urgent' : 'queue' } }];"
      }
    },
    {
      "id": "6",
      "name": "Urgent?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        920,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.route }}",
              "rightValue": "urgent",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "7",
      "name": "Flag for a person now",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1140,
        200
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "action",
              "value": "alert_on_call",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      }
    },
    {
      "id": "8",
      "name": "Queue for daily batch",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1140,
        420
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a2",
              "name": "action",
              "value": "={{ $json.route === 'drop' ? 'dropped_spam' : 'queued' }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      }
    },
    {
      "id": "10",
      "name": "Reject bad input",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        440,
        520
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { error: 'invalid_input', detail: $json.error?.message || 'need subject or body' } }}",
        "options": {
          "responseCode": 400
        }
      }
    },
    {
      "id": "9",
      "name": "Respond",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1360,
        300
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { category: $json.category, urgency: $json.urgency, summary: $json.summary, needs_reply: $json.needs_reply, confidence: $json.confidence, source: $json.source, action: $json.action, dedupeKey: $json.dedupeKey } }}",
        "options": {}
      }
    }
  ],
  "connections": {
    "Inbound message": {
      "main": [
        [
          {
            "node": "Normalize + dedupe key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize + dedupe key": {
      "main": [
        [
          {
            "node": "LLM classify",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject bad input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM classify": {
      "main": [
        [
          {
            "node": "Validate model output",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fallback rules (LLM down)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate model output": {
      "main": [
        [
          {
            "node": "Urgent?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fallback rules (LLM down)": {
      "main": [
        [
          {
            "node": "Urgent?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Urgent?": {
      "main": [
        [
          {
            "node": "Flag for a person now",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Queue for daily batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag for a person now": {
      "main": [
        [
          {
            "node": "Respond",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Queue for daily batch": {
      "main": [
        [
          {
            "node": "Respond",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}