Skip to content
Vince.
All case studies

AI automation · n8n · Workflows

Lead triage

LLM classification, rule-based routing, packaged for n8n

Inbound leads classified by a model against closed vocabularies — five labels, three urgencies — and routed by tested rules: spam is archived even when it shouts, urgent support escalates, sales goes to the pipeline, and anything the validator does not recognise goes to a human. Packaged as an n8n workflow that runs end to end on a laptop with zero accounts.

  • n8n
  • Gemini API
  • Python
  • Google Sheets

Repository: lead-triage

In short

  • The model answers one narrow question — what is this about, and can it wait; routing policy lives in tested code, not in a prompt where nobody can test it
  • Answers outside the closed vocabulary route to review — an unexpected model output is a reason for a human to look, never a reason to guess
  • The n8n workflow JSONs are generated from the Python package, so the prompt the flow ships is the prompt the evals measured — a test fails if they drift
  • 34 tests plus a twelve-lead eval harness scoring label, urgency and route, no API key required

What the model is allowed to decide

The classifier returns a label from a vocabulary of five and an urgency from a vocabulary of three, or it returns an error. There is no "the model said 'Sales inquiry', close enough" — the workflow's Switch node matches exact strings, and close enough is exactly where a pipeline goes silently empty. Everything deterministic lives outside the prompt: normalisation knows that 0917 123 4567 and +639171234567 are the same caller, and the routing policy is a total function — every label-urgency pair, including invalid ones, routes somewhere, with the invalid ones routed to a person.

That split is the transferable part. The model is a component with a narrow contract, wrapped in validation, with business policy in code where it can be tested, reviewed and changed without wondering what else the prompt might do differently now.

Workflows as generated artifacts

The n8n workflow JSONs are not hand-maintained — a generator imports the classification prompt from the Python package and emits both flows, so the prompt n8n ships is byte-identical to the prompt the eval suite measured. A test regenerates the workflows and fails if the committed files differ; another walks every connection and fails on a dangling node reference; a third asserts the production flow carries a credential slot and no actual key.

Two variants come out of the generator. The local one runs end to end with zero accounts — a mock Gemini server answers the same wire format and a recording sink stands in for the spreadsheet — which is the same discipline as the backend repositories, where every suite runs with no broker and no Docker. The production variant swaps in real Gemini and Google Sheets appends, with an urgent-lead escalation email.

Why this shape and not a bigger one

Triage is the automation with the best ratio of value to risk: it never answers a customer, so a wrong classification costs a mis-sorted queue entry rather than a wrong promise. The design puts every consequential decision — what escalates, what archives — in code a reviewer can read, and reserves the model for the judgment call software is genuinely bad at.

The same skeleton — webhook in, model answers a narrow question, validated answer drives tested routing, rows land in a sheet — is the shape of most business automation worth building: document intake, ticket routing, order-status inquiries. Swapping the vocabulary and the sinks is configuration; the discipline is the product.

Seeing it run

Captured from an actual run, not an illustration. The repository has the script that produced it.

n8n start · curl POST /webhook/lead-intake ×4 · curl /sink/log
$ python mock/llm_server.py &$ n8n import:workflow --input=workflows/lead-triage.local.jsonSuccessfully imported 1 workflow.$ n8n start & $ curl -X POST http://localhost:5678/webhook/lead-intake -d '{    "name": "Maria  Santos", "phone": "0917 123 4567",    "message": "Interested po ako sa pricing ng catering package" }'  ... and three more: an outage, an obvious scam, a casual question $ curl -s http://127.0.0.1:8787/sink/log   route            label/urgency    lead  sales_pipeline   sales/normal     Maria Santos    +639171234567  escalate         support/urgent   Jun Reyes       +639181234567  archive          spam/urgent      WINNER  inbox            other/low        D. Villanueva   +639661234567
Four leads through a real n8n engine — webhook to normalisation to LLM to validation to routed sink, zero accounts. The phones went in as 0917 123 4567 and came out +63: the normalisation ran inside the flow, not just in the test suite.

Source

  • triage/rules.py
  • triage/classify.py
  • workflows/generate.py
  • evals/run_evals.py