An n8n workflow that triages inbound leads with Claude, built on one rule most "AI + n8n" projects get wrong: n8n routes, the AI decides, and the intelligence lives in a tested service, not in a node.
Drop an LLM prompt straight into a workflow and you get something that demos well and rots fast: no tests, no versioning, no way to see why it sent a hot lead to spam, and a silent failure the day the API hiccups. This puts the model behind a small HTTP service with a schema, guardrails and a test suite, and lets n8n do what n8n is great at: receive, route, and fan out to your tools.
The actual workflow running in n8n. A lead hits the webhook, the HTTP node asks the brain, and the Switch routes on one field.
- Any source POSTs a lead to the n8n Webhook (
/lead-intake): a website form, a Typeform, an email parser, a CRM. - An HTTP Request node calls the brain service with the lead.
- The brain classifies it (sales, support, spam, other), extracts what
it can (company, budget, product interest), and returns a single
actionstring. - A Switch routes on that
actionto four branches: sales alert, support ticket, human review, or silent drop. You attach your own Slack, Gmail, HubSpot or Sheets nodes at the ends.
The Switch never sees the model. It switches on one clean field, which is why the workflow stays a dumb, reliable router instead of a place where AI logic quietly accumulates.
The brain (brain/app) asks Claude to classify and extract, and then
code, not the prompt, decides the route (schema.py: decide_action).
That separation is what makes the two guardrails possible:
- Low confidence routes to a human. Below a configurable floor
(default 0.6) the lead goes to
needs_humanno matter how the model labelled it. Routing a hot lead to the wrong bucket costs a sale; routing an unsure one to a person costs a minute. - A model failure also routes to a human, never to the void. API
down, broken JSON, an intent outside the allowed set: each is caught,
attached as a reason, and sent to
needs_human. A triage bot that can silently drop a lead when its model stumbles is a liability with a webhook, not an automation.
Both are tested, and both are visible in the response's reasons field,
so a human reviewing the queue sees exactly why the lead landed there.
docker compose up -d --build # demo mode, no API key needed
That starts n8n (http://localhost:5678) and the brain together. In demo mode the brain uses a keyword heuristic that clearly labels itself, so you can exercise the entire workflow for free. For the real classifier:
LLM_MODE=claude ANTHROPIC_API_KEY=sk-... docker compose up -d --build
Import workflows/lead-triage.json into n8n, activate it, and send a
lead. This is a real captured run of the live stack:
$ curl -s -X POST localhost:5678/webhook/lead-intake \
-H 'Content-Type: application/json' \
-d '{"message":"Hi, can I get pricing for 200 units? We are Acme Foods.","email":"buyer@acme.com"}'
{ "channel": "sales",
"notify": "[NORMAL] New lead: ... pricing for 200 units? We are Acme Foods.",
"decision": { "action": "route_sales", "intent": "sales",
"confidence": 0.9, "reasons": [] } }
And the guardrail firing on a vague message, captured the same way:
$ curl ... -d '{"message":"hello there"}'
{ "channel": "human_review",
"decision": { "action": "needs_human",
"reasons": ["confidence 0.50 below floor 0.6"] } }
All four routes (sales, support, drop, human) were verified end to end through the live n8n webhook, not just against the service in isolation.
workflows/
lead-triage.json the main workflow (webhook -> brain -> switch -> 4 routes)
error-handler.json an Error Trigger workflow, so a workflow-level
failure pings a person instead of dying quietly
brain/
app/schema.py intents, actions, and decide_action (the routing policy)
app/llm.py Claude, a key-free demo brain, a scripted test double,
and strict validation of the model's reply
app/triage.py the engine: classify, validate, route, never lose a lead
app/main.py the FastAPI service n8n calls
tests/ 17 tests, no API key, no network
docker-compose.yml n8n + brain, wired together
cd brain && pip install -r requirements.txt && python -m pytest
17 deterministic tests, no key and no network. They cover the routing policy, the confidence floor, every failure mode (model crash, invalid intent, non-numeric confidence) routing to a human, input limits enforced at the HTTP door before any model call, and the full demo loop end to end.
- Change the categories. Edit
INTENTS,ACTIONSanddecide_actioninschema.py, and add a Switch output in n8n. The prompt is built from the schema, so the model follows. - Attach your tools. Each branch ends in a Set node shaping a
notification payload. Replace those ends with your Slack, Gmail, HubSpot
or Sheets nodes. The
dropbranch stays silent on purpose. - Tune the guardrail.
CONFIDENCE_FLOORdecides how cautious triage is. Raise it and more leads get a human glance; lower it and the model acts on its own more often.
This is the triage layer, not a CRM. It decides where a lead goes and why, and hands off cleanly; it does not store leads or send the notifications itself, because in n8n those are nodes you already have and want to own. The brain is stateless on purpose: every decision depends only on the message in front of it, which is what keeps it testable.

