Score and route inbound leads
The job: a form fill arrives. Before it reaches a rep, decide whether it is worth a call, and who should get it — using what that person did on your site before they filled the form.
Who this is for: anyone whose reps complain that inbound is mostly junk.
The idea
A form tells you who someone claims to be. Signal tells you what they did for the three weeks before they filled it in. The second is the better predictor and you already have it.
Enriching a form fill
def enrich(email: str) -> dict | None:
r = requests.get(f"{BASE}/visitors",
params={"q": email, "page_size": 1}, headers=H)
r.raise_for_status()
hits = r.json()["visitors"]
return hits[0] if hits else None
A miss is common and informative: someone who filled a form on their first visit has no history, which is itself a weaker signal than someone who read you for a month first.
A score you can defend
def score(form, visitor):
if visitor is None:
return 20, "no prior visits"
pts, why = visitor["intent_score"] // 2, []
if visitor["visit_count"] >= 5:
pts += 20; why.append(f"{visitor['visit_count']} visits")
if visitor["classification"] == "customer":
pts += 30; why.append("existing customer")
if visitor["classification"] in ("competitor", "excluded"):
return 0, visitor["classification"]
if visitor["company_domain"] == "personal":
pts -= 20; why.append("consumer mailbox")
return min(pts, 100), ", ".join(why) or "no strong signal"
Returning the reason alongside the number is the part people skip. A rep who can see "existing customer, 9 visits" trusts a 90 in a way they never trust a bare 90.
Routing
points, reason = score(form, enrich(form["email"]))
if points >= 70: assign(form, team="enterprise", sla_minutes=15)
elif points >= 40: assign(form, team="smb", sla_minutes=240)
else: nurture(form, reason=reason)
Letting a model handle the edges
Thresholds handle the middle well and the edges badly. For anything that lands in a band you have flagged as uncertain, ask:
A form was submitted:
{form_json}
Here is what this person did on our site beforehand:
{visitor_json}
{journey_json}
Decide: enterprise, smb, or nurture. Give one sentence of reasoning that cites
specific behaviour. If the visitor record is empty, say so and route on the
form alone — do not infer behaviour that is not there.
What will go wrong
The email does not match. People fill forms with a personal address and browse from a work one, or the reverse. You will miss history you have. Match on company domain as a fallback, and accept the miss rate.
Competitors fill forms. They do, constantly, and they read your pricing
page more carefully than your buyers. classification: competitor is the cheap
fix; it needs someone to have classified them once.
Scores drift. The weights above are guesses on day one. Store the score and the reason with the lead, then look at what actually closed in ninety days and change them. A scoring model nobody revisits is astrology with arithmetic.
Next: build a daily call list · alert your team when a target account appears
Markdown source: /developers/workflows/score-and-route-inbound-leads.md