Lead triage: one form, three outcomes
A lead arrives from a website form. A model reads the message and scores its intent and urgency. A few lines of code turn that score into one of three tiers, and a switch sends each tier down its own branch: hot books a slot and alerts the owner, warm starts a nurture, junk goes to its own branch. The page that sent the lead gets an answer either way.
Demo Built to prove the mechanism, not run for a paying client. The workflow below is the real export; credentials and personal details are removed and nothing else is.
- nodes
- 12
- tiers
- 3
- model call
- 1
- score fields
- 4
- reply node
- 1
- n8n
- OpenAI gpt-4o-mini
- GoHighLevel API
- Gmail
How it works
A form posts the lead to a webhook. The lead is cleaned and capped. A lead with no usable email or phone number goes straight to junk without a model call; every other lead goes to the model with one instruction: judge only from the message. The model answers in a fixed shape. Code reads that answer and picks hot, warm or junk. A switch with three outputs sends the lead down its branch, and all three branches meet again at the one node that answers the form.
The model scores. Code decides.
The model never picks the branch. It returns four fields under a strict JSON schema, at temperature 0, and three lines of plain code turn those fields into a tier. Want urgency 3 to count as hot? That is one number in one line of code, not a rewritten prompt and a week of hoping the model reads it the same way every time.
| Field | Type | What the model is told |
|---|---|---|
intent | one of: emergency, quote, question, spam | What the person wants |
urgency | whole number, 1 to 5 | 5 means active damage or danger, 1 means idle curiosity |
is_junk | true or false | True for spam, sales pitches and gibberish |
reason | text | One line on why, which ends up in the owner alert |
Five things that would otherwise go wrong
A lead nobody can contact never reaches the model
Has Contact? sits straight after Clean And Validate. A lead with no name, or with neither a real looking email nor a phone number, goes to Log And Drop without a model call. Nobody pays to score a lead that cannot be answered, and an urgent sounding message with no way to reply can never book a slot.
A model outage turns into a warm lead, not a lost one
Score The Intent is set to continue on error. If the model API is down or times out, the run carries on, Decide The Tier finds no reply it can read, and the lead lands in the warm nurture. The form still gets its answer.
An unreadable model reply still lands somewhere
Decide The Tier parses the model's reply inside a try. If the reply cannot be read, the score falls back to a question with urgency 2, which is warm. A real lead goes into the nurture instead of disappearing.
The model only ever sees a bounded input
Before the model call, the name is cut to 80 characters, the message to 2,000, the phone number to digits and a plus sign, and the email is lowercased. One pasted essay cannot run up the bill, and every lead costs about the same to score.
Every branch answers the form
Hot, warm and junk all end at the same Respond to Webhook node, which returns ok and the tier. The person who filled in the form is never left looking at a spinner because their lead took the junk branch.
The twelve nodes, in four steps
Take the lead in and clean it
Lead Submitted is a webhook that accepts a POST and is set to answer through a Respond to Webhook node, so the form waits for the real result. Clean And Validate trims every field, applies the caps above, and checks that there is a name and either an email that looks real or a phone number. Has Contact? is an IF node: a lead that failed that check goes straight to Log And Drop, and every other lead goes on to be scored.
Score the message
Score The Intent sends the name and the message to gpt-4o-mini with a short system instruction: you triage inbound leads for a service business, judge only from the message, never invent detail. The request asks for the four fields above as strict JSON, at temperature 0, capped at 200 tokens. The node is set to continue on error, so an outage never stops the run.
Decide the tier, then route it
Decide The Tier applies the rules below in order. Route By Tier is a switch with three named outputs that matches the tier exactly.
If the score says The tier is is_junk is true, or intent is spam junk urgency is 4 or 5, or intent is emergency hot anything else, including a reply that could not be read warm Act on it, then answer the form
Tier Nodes What happens Hot Book The Slot, Alert The Owner A booking request goes to the GoHighLevel calendar API with the contact, the urgency and the reason. Then the owner gets an email headed "HOT lead", with the intent, urgency, the reason and the full message. Warm Start The Nurture, Tag For Follow Up A contact is created in GoHighLevel tagged warm plus the intent, and the lead is marked for follow up 24 hours later. Junk Log And Drop Reached two ways: from the switch, or straight from Has Contact? for a lead with no contact details. Only the reason, the name, the message and a timestamp are kept. Nothing is sent to anyone. All three Tell The Page It Worked Returns ok and the tier to the form that sent the lead.
Three n8n details that are easy to get wrong
The webhook has to be told to wait
A Respond to Webhook node only answers if the webhook's Respond setting says to use it. Leave the webhook on its default and n8n answers the form straight away, before the lead has even been scored, so the form never learns the tier.
After an API call, $json is the API's answer
Once Book The Slot has run, the item flowing on is the calendar API's response, not the lead. So Alert The Owner reads the name, urgency and message from $("Decide The Tier") by name. Point it at $json and the alert arrives with the fields empty.
Strict JSON can still arrive broken
A strict schema fixes the shape of the answer, but a reply that hits the 200 token cap is cut off mid-object and will not parse. That is why the parse sits in a try with a warm fallback rather than being trusted.
What this demo does not do yet
Labelled honestly, the same as everything else on this site. A client build closes both.
The CRM calls carry the contact, not the account
Book The Slot and Start The Nurture send the contact's details only. A real build adds the calendar, the time slot, the GoHighLevel location and the API credential for the client's own account.
Junk is kept on the run, not in a table
Log And Drop shapes a record, but nothing writes it anywhere. It survives as long as n8n keeps the execution. A real build writes it to a sheet or a table, so a lead wrongly scored as junk can be found and rescued.
What changes for a real business
- What counts as hot. One line in Decide The Tier. A plumber's emergency and a law firm's emergency are not the same word.
- The categories. The intent list is four words in the schema. Swap them for the jobs you actually quote.
- Where hot goes. Gmail is one node. It can just as well be a text message, a Slack channel or a call task.
- The CRM. The two GoHighLevel calls are plain HTTP requests, so HubSpot or any CRM with an API takes their place.
Build checklist
- The webhook answers through the Respond to Webhook node, not straight away.
- Test leads for all three tiers, plus one with no contact details, one with a broken model reply, and one with the model unreachable.
- The model credential, the CRM credential and the alert inbox are the client's own.
- The hot threshold and the intent list are agreed with whoever answers the phone.
- Junk lands somewhere a person can look at it.
- The form shows a sensible message for every tier, and for an error.
Want this on your own enquiries?
Thirty minutes, no deck. If I cannot see anything worth automating, I will tell you that.