{
  "name": "AI Classify & Route",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "classify",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        280,
        280
      ],
      "notesInFlow": true,
      "notes": "Swap for Gmail / Telegram / form trigger"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4o-mini",
          "mode": "list",
          "cachedResultName": "gpt-4o-mini"
        },
        "messages": {
          "values": [
            {
              "content": "Classify the message into exactly one of: sales, support, billing, spam.\n\nReply with the single word and nothing else. No punctuation, no explanation.\nIf the message fits none of them, reply: other",
              "role": "system"
            },
            {
              "content": "={{ $json.body?.message || $json.message || $json.text }}"
            }
          ]
        },
        "options": {
          "temperature": 0
        }
      },
      "id": "classify",
      "name": "Classify",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        520,
        280
      ],
      "notesInFlow": true,
      "notes": "Small model is plenty for classification"
    },
    {
      "parameters": {
        "jsCode": "// Never trust the label unchecked. Models add punctuation, capitals, and the\n// occasional full sentence — and a Switch node matches none of those.\nconst ALLOWED = ['sales', 'support', 'billing', 'spam'];\n\nreturn $input.all().map((item) => {\n  const raw = item.json.message?.content ?? item.json.content ?? item.json.text ?? '';\n  const cleaned = String(raw).toLowerCase().replace(/[^a-z]/g, '');\n  const category = ALLOWED.includes(cleaned) ? cleaned : 'review';\n\n  return {\n    json: {\n      category,\n      rawLabel: String(raw).trim(),\n      // True when the model returned something outside the list. Worth watching:\n      // a rising rate here means the prompt or the categories need work.\n      offList: category === 'review',\n      original: $('Webhook').item.json.body ?? $('Webhook').item.json,\n    },\n  };\n});"
      },
      "id": "normalise",
      "name": "Normalise Label",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        280
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "c-sales",
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "sales",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "sales"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "c-support",
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "support",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "support"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "c-billing",
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "billing",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "billing"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "c-spam",
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "spam",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "spam"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "review"
        }
      },
      "id": "route",
      "name": "Route By Category",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1120,
        280
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": ""
        },
        "text": "=🟢 New sales enquiry\n\n{{ JSON.stringify($json.original) }}",
        "otherOptions": {}
      },
      "id": "sales-slack",
      "name": "Sales → Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1400,
        60
      ]
    },
    {
      "parameters": {},
      "id": "support-branch",
      "name": "Support → Your Handler",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1400,
        200
      ]
    },
    {
      "parameters": {},
      "id": "billing-branch",
      "name": "Billing → Your Handler",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1400,
        320
      ]
    },
    {
      "parameters": {},
      "id": "spam-branch",
      "name": "Spam → Drop",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1400,
        440
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "YOUR_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "needs_review",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "id": "review-queue",
      "name": "Review Queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1400,
        580
      ]
    },
    {
      "parameters": {
        "content": "## Classify inbound messages with AI and route each category to its own branch\n\nOne model call decides where a message goes — sales, support, billing or spam — and anything the model\ncannot place lands in a review queue instead of vanishing.\n\nTriage is the highest-value use of an LLM in an automation and the cheapest to run. Nothing here is\ngenerated, so a small model is enough and the cost stays in fractions of a cent per message.\n\n**Why a classifier and not an agent.** The steps are fixed; you already know the order. An agent would\npay a model to rediscover your own flowchart on every run. Reach for one only when the path genuinely\nvaries between runs.\n\n### How it works\n\n1. A webhook receives the message. Swap it for a Gmail, Telegram or form trigger without touching\n   anything downstream.\n2. An OpenAI node classifies the text into exactly one of four categories at temperature 0, so the same\n   input keeps returning the same label.\n3. A Code node normalises the reply — lowercased, stripped to letters, and anything off-list rewritten\n   to `review`.\n4. A Switch node routes on that label, one output per category plus a named `review` fallback.\n5. Sales posts to Slack, support and billing hand off to your own handlers, spam stops, and everything\n   unrecognised is appended to a Google Sheet.\n\n### Setup steps\n\n- [ ] Add OpenAI credentials, and set a spending limit at the provider before you publish this.\n- [ ] Add Slack credentials and choose the channel on **Sales → Slack**.\n- [ ] Add Google Sheets credentials and replace `YOUR_SHEET_ID` on **Review Queue**. It appends to a\n      tab named `needs_review`.\n- [ ] Replace the two **Your Handler** no-op nodes with what should actually happen.\n- [ ] POST `{ \"message\": \"...\" }` to the webhook and confirm it lands on the branch you expect.",
        "height": 1160,
        "width": 480
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -320,
        -60
      ],
      "id": "sticky-main",
      "name": "sticky-main"
    },
    {
      "parameters": {
        "content": "## Receive and classify\n\nThe webhook is a placeholder for whatever carries your messages. One model call, temperature 0, no\ngeneration — it returns a single word.",
        "height": 432,
        "width": 560
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        240,
        56
      ],
      "id": "sticky-recv",
      "name": "sticky-recv"
    },
    {
      "parameters": {
        "content": "## Normalise, then route\n\nModels add capitals, punctuation and the occasional full sentence, and a Switch node matches none of\nthose. Clean the label first, route second.",
        "height": 432,
        "width": 440
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        820,
        56
      ],
      "id": "sticky-route",
      "name": "sticky-route"
    },
    {
      "parameters": {
        "content": "## One branch per category\n\nEach output is a separate, editable path. The two no-op nodes are deliberate placeholders — swap in a\nticket, a CRM record or an email and the rest of the workflow is unchanged.",
        "height": 960,
        "width": 460
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1340,
        -184
      ],
      "id": "sticky-fan",
      "name": "sticky-fan"
    },
    {
      "parameters": {
        "content": "## ⚠️ The fallback branch is the point\n\nThe model will occasionally return something outside your list. Without a fallback those items vanish\nsilently, and you find out weeks later that you lost sales leads.\n\nRouting them to a review queue instead means an off-list answer costs you one glance, not a customer.\n**Watch the rate.** If it climbs past a percent or two, your categories are ambiguous — that is a\nprompt problem, not a model problem.",
        "height": 320,
        "width": 880
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        240,
        560
      ],
      "id": "sticky-warn",
      "name": "sticky-warn"
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Classify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify": {
      "main": [
        [
          {
            "node": "Normalise Label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalise Label": {
      "main": [
        [
          {
            "node": "Route By Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route By Category": {
      "main": [
        [
          {
            "node": "Sales → Slack",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Support → Your Handler",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Billing → Your Handler",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Spam → Drop",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Review Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
