A UK accountancy practice was routing all inbound enquiries the same way: every call went to the front desk, every form submission went to a shared inbox, every email was read by whoever checked the inbox first. Two fee-earners handled all follow-up. Conversion from enquiry to booked appointment was 18%.
We spent 90 minutes with the founder mapping out what she actually wanted to happen. Small business owner enquiring about accounts for the first time → instant Calendly booking link, low-urgency. Existing client reporting a tax problem → immediate phone callback, high-urgency. Prospect from a specific postcode (near a new office location they'd advertised in) → route to the location-specific advisor. Company over 10 employees → route to the commercial team, not personal accounts.
It took 90 minutes to articulate. It took 3 hours to build in YAML and wire to their inbound stack. Conversion went to 31% in month one. The routing decision had always existed — it just lived in the founder's head, unavailable to any system.
Every inbound lead routing decision can be expressed as a decision tree. The work of building the tree is mostly the work of making the decision explicit.
Why routing logic lives in people's heads and why that's a problem
Most UK SMEs with active inbound leads have implicit routing logic that works something like: "if it's Sarah, she handles the big ones; if it's a form fill about pricing, John takes it; if they mention they found us on Google, we prioritise them."
That logic doesn't survive holidays, sick days, or team changes. It's not available to any automated system. It can't be tested, tuned, or measured. And it produces inconsistent outcomes — not because the people are inconsistent, but because the logic is undocumented and therefore applied differently by different people under different conditions.
The decision tree is the practice of writing down what's actually in people's heads and encoding it in a system that applies it consistently, 24 hours a day, without requiring anyone to be at their desk.
The research that prompted the Forrester analysis on B2B lead routing (2023) found that companies with documented, automated routing logic converted inbound leads at 2.1× the rate of companies with ad-hoc routing. The gap isn't quality of lead or quality of rep — it's response time and context.
A counterpoint worth reading: HubSpot's 2024 Sales Trends Report found that companies with complex, multi-criteria routing rules saw lower lead-to-meeting conversion in some segments than companies using simpler first-available-rep routing. The reason was latency — the more conditions in the routing logic, the more time passed before a human contacted the prospect. The lesson isn't that simple is always better; it's that routing precision is only worth the delay it introduces. For inbound enquiries where speed matters most, test your routing tree's average evaluation time (including enrichment) against a simpler fallback. The ICO's guidance on automated decision-making is also worth reviewing — if your routing tree is making decisions about which leads to disqualify automatically, document the logic and ensure there's a human review mechanism for edge cases.
The inputs: channel, intent signal, company size, time of day
A routing decision needs inputs. The more inputs you have at routing time, the more precise the routing can be. The inputs available depend on your inbound stack:
| Input | Source | Available at routing time? |
|---|---|---|
| Channel (phone / form / email / chat) | Your inbound system | Always |
| Intent signal (free text or form field) | Form submission or transcript | Usually |
| Company name / domain | Form field or caller ID lookup | Often |
| Company size (headcount) | Enrichment API (Apollo, Clay) | With 1–3s delay |
| Industry | Enrichment API | With 1–3s delay |
| Time of day / day of week | System clock | Always |
| Prior CRM record | CRM lookup by email/phone | With 0.5–1s delay |
| ICP score (pre-computed) | CRM record | If previously scored |
The minimal viable routing inputs — the ones available on every inbound lead with no enrichment required — are channel and time of day. Combined with a form intent field or a voice agent's first-turn classification, these three inputs can determine routing for 70–80% of enquiries without any external API calls.
Enrichment-dependent routing (company size, industry) is more precise but adds latency. Design the tree to work without enrichment (using graceful-degradation fallback nodes) and add enrichment-based branching where the precision is worth the delay.
The routing outputs: instant book, qualify call, nurture sequence, disqualify
Four terminal outputs cover the vast majority of UK B2B inbound routing scenarios:
Instant book: The lead meets ICP criteria and has expressed specific intent. Send a booking link immediately (via SMS or email) or connect to a voice agent that offers slots directly. Target: reply within 90 seconds.
Qualify call: The lead shows intent but needs qualification before booking. A voice agent calls back within 5 minutes. Target: callback within 5 minutes of enquiry.
Nurture sequence: The lead shows interest but is not ready to buy. Enter a 4–6 touch email sequence with educational content. Target: no immediate outreach; first email within 1 hour.
Disqualify: The lead is outside your ICP or the enquiry is non-commercial (job applications, press, suppliers). Log it, send an appropriate response, do not route to sales.
The map between inputs and outputs is the tree.
Building the decision tree in YAML / JSON (version-controllable routing)
Here's a working routing tree for a UK professional services firm, in a YAML schema we use in production:
routing_tree:
id: root
input: channel
branches:
- match: phone
next:
id: phone_time_check
input: time_of_day
branches:
- match: business_hours
next:
id: phone_intent
input: voice_agent_classification
branches:
- match: new_enquiry_high_intent
output: qualify_call
sla_minutes: 0 # voice agent handles live
- match: new_enquiry_low_intent
output: nurture_sequence
sla_minutes: 60
- match: existing_client_urgent
output: instant_book
sla_minutes: 2
- match: default
output: qualify_call
sla_minutes: 5
- match: out_of_hours
output: queue_for_morning
sla_minutes: 480 # next business hour
- match: form
next:
id: form_enrichment
input: company_headcount
branches:
- match: gte_20
next:
id: form_icp_check
input: industry
branches:
- match: [financial_services, legal, accountancy]
output: qualify_call
sla_minutes: 5
- match: default
output: instant_book
sla_minutes: 10
- match: lt_20
output: nurture_sequence
sla_minutes: 60
- match: unknown # enrichment failed
output: qualify_call
sla_minutes: 15
- match: email
next:
id: email_intent
input: subject_classifier
branches:
- match: pricing_enquiry
output: qualify_call
sla_minutes: 30
- match: general_enquiry
output: nurture_sequence
sla_minutes: 120
- match: default
output: nurture_sequence
sla_minutes: 120
This tree is under 80 lines, handles the three primary inbound channels, degrades gracefully when enrichment is unavailable, and produces a specific output with an SLA for each path. Every output is actionable by the automation layer.
The value of YAML over a spreadsheet or a flowchart: it's version-controlled in git. Every change has a commit message and a diff. You can roll back a routing change that produced a bad outcome. You can run tests against it.
CRM integration: reading enrichment data at routing time
For tree branches that depend on company size or industry, the evaluation engine calls your enrichment provider at routing time:
async def enrich_contact(email: str, timeout_seconds: float = 3.0) -> dict:
try:
result = await asyncio.wait_for(
apollo_client.enrich(email=email),
timeout=timeout_seconds
)
return {
"headcount": result.get("headcount"),
"industry": result.get("industry"),
"country": result.get("country"),
"confidence": result.get("confidence", 0),
}
except asyncio.TimeoutError:
return {"headcount": None, "industry": None, "confidence": 0}
def evaluate_tree(node: dict, context: dict) -> dict:
if node.get("output"):
return node # terminal node
input_val = context.get(node["input"])
if input_val is None:
# Fall back to default branch if input not available
default = next((b for b in node["branches"] if b["match"] == "default"), None)
return evaluate_tree(default["next"] if "next" in default else default, context)
for branch in node["branches"]:
if matches(branch["match"], input_val):
next_node = branch.get("next", branch)
return evaluate_tree(next_node, context)
The timeout_seconds=3.0 parameter is important. If enrichment doesn't return within 3 seconds, the tree falls back to the unknown/default branch rather than blocking the routing decision. A lead waiting 8 seconds for routing is a lead that thinks the form didn't submit.
Write the enrichment result back to the CRM record after routing, regardless of whether it influenced the decision. The data is useful for scoring and attribution later. For more detail on the enrichment data model and ICP scoring patterns, see the CRM enrichment and ICP scoring guide. The speed-to-lead guide covers what happens after routing completes — the response-time window the routed rep is working within.
Voice agent as the routing layer for phone inbound
For phone inbound, the routing decision happens inside the voice agent's first two turns. The agent answers, asks a qualifying question, classifies the intent, and routes accordingly — all within 30–45 seconds.
The agent's first turn classification maps directly to the tree:
{
"classification_prompt": "Classify the caller's intent based on their first response. Options: new_enquiry_high_intent (mentions specific product or wants to book), new_enquiry_low_intent (general interest, no specific ask), existing_client_urgent (mentions an existing relationship and a problem), not_commercial (job enquiry, press, supplier). Respond with only the classification label.",
"fallback_classification": "new_enquiry_low_intent"
}
The classification fires after the caller's first full response. The routing output determines what the agent says next:
new_enquiry_high_intent→ agent offers two or three available appointment slots directly ("I have availability Tuesday at 2pm or Thursday at 10am — which works better?")new_enquiry_low_intent→ agent captures name and email, sends instant booking link via SMSexisting_client_urgent→ agent transfers to duty rep immediatelynot_commercial→ agent handles politely and ends call
The voice agent is the routing layer for phone inbound. It doesn't need a human to make the routing decision — it makes the decision itself based on the classification, which is grounded in the YAML tree. The LinkedIn AI SDR case study shows how inbound routing integrates with an outbound prospecting system — when an SDR-touched prospect calls inbound, the routing tree checks their prior sequence history in CRM and routes to the assigned rep rather than the general queue.
We covered the broader inbound routing architecture in our AI inbound lead routing guide.
Edge cases: incomplete data, off-hours, multiple simultaneous leads
Incomplete data: Covered above — every tree node has a match: default or match: unknown branch that handles missing inputs. Don't leave a path in the tree that produces no output.
Off-hours: The time_of_day input distinguishes business hours from outside. Off-hours routing options: queue for morning (with automated acknowledgement message), route to an always-on voice agent (for time-critical industries like legal or property), or direct to an on-call rep for urgent cases. Define "urgent" in the tree — typically anything from existing clients or prospects who've explicitly mentioned time-sensitivity.
Multiple simultaneous leads from the same company: If two people from the same company submit forms or call within an hour, the second lead evaluation should check the CRM for a concurrent lead from the same organisation and route to the same rep rather than creating a second sequence. Add a company-level dedup check before the tree evaluation:
def route_lead(lead: dict) -> dict:
# Check for concurrent leads from same company
if lead.get("company_domain"):
recent = crm.find_recent_leads(
domain=lead["company_domain"],
within_hours=24
)
if recent:
return {"output": "assign_to_existing_rep", "rep_id": recent[0]["owner_id"]}
# Run tree evaluation
return evaluate_tree(routing_tree["routing_tree"], enrich_context(lead))
What changed in 2025–2026: LLM-assisted intent classification
Until 2025, intent classification at routing time was regex-based (keyword matching on form fields) or simple ML classifiers trained on historical lead data. The quality was adequate but brittle — a prospect who wrote "wondering if you do the thing where AI answers calls" wouldn't match a keyword classifier looking for "voice agent".
In 2026, LLM-based intent classification at routing time is fast enough and cheap enough to use on every inbound lead. A Claude Haiku or GPT-4o-mini call to classify a form submission or first voice agent turn adds 200–400ms and costs £0.001–0.003 per lead — negligible at SME volumes.
The accuracy improvement is substantial: in A/B tests we ran across four client deployments, LLM classification improved correct routing rate from 71% (keyword classifier) to 89% (LLM classifier) on ambiguous form submissions. For clear, explicit enquiries the difference was minimal; for ambiguous or colloquial language it was significant.
Good / Bad / Ugly
Good: A YAML routing tree in git, evaluated by a Python function, with enrichment API calls and graceful fallback. Every branch tested against historical lead data before deployment. SLAs attached to every terminal node. Voice agent classification for phone inbound. Conversion rate 28–34%.
Bad: A routing "system" that's actually a spreadsheet with rep names and a note saying "James handles big ones." No SLA tracking. No suppression for off-hours. No fallback for when James is on holiday. Response time varies between 4 minutes and 4 hours depending on who saw the lead first.
Ugly: A beautiful Miro flowchart of the routing logic that nobody implemented in any system. Reps route manually by referring to the Miro board. The board was last updated 6 months ago and no longer reflects current team structure. Three leads per week go to a rep who left in March.
The decision tree is not the flowchart. The flowchart is the documentation. The tree is the code that runs.