Free kit / Automated Lead Routing Kit

Automated Lead Routing: Every Lead Owned in Under Ten Seconds

I wrote this routing layer for a firm running twelve intake channels across three offices. You get the contract payload and the priority order. The decision object comes out of them, and the failure queue catches whatever stays unresolved.

12failure signals, each with a lookup location
10worksheets you fill in on your own stack
8sections, in the order that makes the diagnosis

Where the routing layer breaks and how I rebuild it is in Automated Lead Routing in CRM. Related kits: the Routing Stack bundles this with the speed-to-lead pipeline, the CRM Automation Repair Kit fixes the duplicates and routing rules inside your CRM, and the Workflow Audit Kit finds where leads are dying before you rebuild anything.

01

Before you start

This is the routing layer I rebuild so every lead that reaches your CRM has an owner attached to it. Same contract, same priority order, same decision object I install for clients. Work the sections in order. The order is the architecture.

Every unplaced lead surfaces in the failure queue with its reason. The form fires, the CRM creates a contact, the dashboard shows activity. Then the owner field stays empty, the deal opens in another pipeline, and the follow-up sequence waits on a routing field that arrived as null. The failure queue catches it before the follow-up window closes.

I wrote this layer for Meridian Legal Group, a firm running twelve intake channels across three offices. Their HubSpot instance had become a data graveyard, with duplicate contacts in the thousands, leads assigned at random, and SLA timers that ran on paper. The rebuild starts one layer below the alerts. It starts with the contract.

Ground rules

Fix ownership before you touch speed. Ten seconds of ownership only counts when the rep can act on it. Take the contract inventory before you change any workflow. Write down what the form sends while it is still true.

02

Section 1 · The routing contract your form must send

Routing starts before the CRM. Every source, from a Typeform to a chat widget to a referral spreadsheet, has to land in one stable payload. The routing engine routes only what the form sends. Lock the field names first. Everything downstream keys on those names.

Table 1A · Routing contract fields (fill the gaps)

Stable API names that outlast display labels. The CRM UI label changes whenever someone renames a property. The contract stays fixed.

Contract fieldTypeRequiredWhere it comes fromWhat breaks without it
event_idstringyesGenerated at intakeRetries create duplicate assignments
correlation_idstringyesGenerated at intakeNo end-to-end trace across systems
idempotency_keystringyessource_system + source_record_id + emailReplay assigns the same lead twice
source_systemenumyesForm, ad platform, chat, importCampaign routing has nothing to key on
emailstringyesForm fieldDedupe finds no match, so owner resolution stops.
phonestringyes*Form fieldSecond match key gone, SMS path dead
company_domainstringnoEmail domain or enrichmentAccount matching falls back to name only
service_requestedenumyesForm select or extractedService-line routing has no target
territoryenumyesForm, IP, or office mappingLeads land outside the office that owns them

The payload every source has to produce

One shape, regardless of where the lead entered. Tally can feed it, Meta Lead Ads can feed it, a manual import can feed it. The routing engine receives the same clean structure every time.

event_id "evt_01J8XK2M9Q" correlation_id "corr_route_01J8XK2M9Q" idempotency_key "lead_route:web_form:submission_8841:ops@example.com" source_system "web_form" source_record_id "submission_8841" received_at "2026-10-19T14:22:08.418Z" email "ops@example.com" phone "+15551234567" company_domain "example.com" service_requested "crm_workflow_repair" territory "europe" budget_range "5000_15000" urgency "high" lead_score 82 existing_customer false open_deal_exists false consent_to_contact true utm_source "google" utm_campaign "crm_workflow_cluster" pain_summary "Owner assignment fails on web leads. High-intent buyers wait."

Table 1B · Source audit: does each channel honour the contract?

Walk every intake channel and mark what it sends. A channel that cannot fill a required field goes to the failure queue path in Section 5.

Source systemRequired fields presentFields it cannot sendFallback owner / path
Tally contact formall requirednonen/a
Naming rule

The contract name is the API name. service_requested is the contract. "What can we help you with?" is a form label someone will rename next quarter. Map labels to contract fields at the edge, ahead of the router.

03

Section 2 · Priority order: existing relationship beats round robin

Most stacks have five routing rules fighting each other. Territory says one owner, service line says another, the campaign says another, round robin says another. Without a fixed priority order, routing becomes random with extra steps. This is the hierarchy I install. Read it top to bottom. The first rule that matches wins.

Table 2A · The priority hierarchy

OrderRuleRoutes toWhy it sits here
1Spam filter and invalid contact dataReject log, never the CRMGarbage must not reach a human queue
2Existing customerExisting owner of recordContinuity beats distribution
3Open deal already existsCurrent deal ownerTwo owners on one buyer splits the pipeline
4Critical severity or high lead scorePriority owner or founder reviewIntent beats geography
5Territory or office locationTerritory ownerLocal ownership before generic pools
6Service line or expertisePractice or product ownerThe right specialist, not the nearest rep
7Campaign sourceCampaign ownerAttribution-sensitive paths stay intact
8Round robin poolNext active rep in rotationOnly for genuinely unclaimed leads
9Nothing matchedManual review queueUnresolved leads never disappear
Why existing relationship wins

Round robin ignores context unless you add it. Equal distribution feels fair. It can also send a returning customer to a stranger and an enterprise lead to a junior rep. Existing customer and open deal sit above the pool for exactly that reason.

Table 2B · Your rule map (fill this in)

List every routing rule currently live in the stack, then assign it a row in the hierarchy. Overlaps become visible the moment two rules claim the same lead.

Rule as built todayHierarchy rowConflicts withDecision
If country = IT then owner_rossi5 territoryround robin on web formterritory wins, remove from pool

Rule merge checklist

  • One winner per lead: every test case resolves to exactly one owner, one pipeline, one stage.
  • Existing customer and open deal sit above every distribution rule: round robin never steals a live relationship.
  • Critical severity outranks campaign source: an urgent lead does not wait behind a nurture path.
  • Round robin pool contains active users only: a departed rep is not a fallback owner.
  • Every rule has a written reason string: the CRM will store it as routing_reason in Section 3.
  • Unmatched leads have a destination: the manual review queue is rule 9 by design.
04

Section 3 · The decision object, written once, executed anywhere

Build the decision before any CRM action runs. The rules engine produces one object against one schema. Whatever executes consumes it, whether that is Make, Zapier, n8n, a HubSpot workflow, a GoHighLevel flow or a custom API layer. The executor does not decide. It carries out a decision that already exists.

Routing decision object schema

{ "routing_status": "assigned", "routing_tier": "priority", "target_owner_id": "user_789", "target_team": "europe_intake", "target_pipeline": "new_matter_intake", "target_stage": "qualified_diagnostic", "follow_up_sequence": "speed_to_lead_priority", "manual_review_required": false, "routing_reason": "Lead score 82, high urgency, service line crm_workflow_repair, territory europe, no open deal, owner capacity available.", "trace": { "event_id": "evt_01J8XK2M9Q", "correlation_id": "corr_route_01J8XK2M9Q", "idempotency_key": "lead_route:web_form:submission_8841:ops@example.com" } }

Table 3A · Decision object fields

FieldAllowed valuesSet byConsumed by
routing_statusassigned | review | rejectedrules engineexecutor, audit log
routing_tierpriority | standard | reviewlead score and urgencyfollow-up sequence picker
target_owner_idCRM user idhierarchy row 2 to 8CRM owner write
routing_reasonhuman sentencerules engineCRM note, sales context
manual_review_requiredtrue | falsefailure policyreview queue

Table 3B · routing_reason templates (fill one per hierarchy row)

Sales needs to know why the lead landed there. A written reason turns a mysterious assignment into an inspectable decision. Use one sentence per row, naming the evidence that fired.

Hierarchy rowReason templateExample filled
2 existing customerExisting customer, owner of record is {owner_id}, relationship since {date}.Existing customer, owner of record is user_112, relationship since 2024.
3 open deal
4 critical severity
5 territory
6 service line
7 campaign
8 round robin
9 manual review
Written once, executed anywhere

If the decision object is stable, swapping the executor does not touch business logic. I have moved the same routing layer between Make scenarios, a HubSpot workflow and a custom Node service without rewriting a single rule. The rules live in the decision. The executor is plumbing.

05

Section 4 · Validate the owner, write to CRM, then alert

Order matters more than speed here. Deduplicate before assignment, never after. Validate the owner and the pipeline before the write. Send the alert only after the CRM confirms. An alert that fires before the write is a lie the sales team will believe.

STEP 1

Store the raw event

What I check: the raw payload lands in storage before any transform runs.

Evidence to collect: the archived payload with event_id and received_at intact.

Pass condition: every source event is reconstructable even if the chain fails mid-flight.

Common failure: the webhook handler parses first and stores never. Then a vendor payload change destroys the evidence.

STEP 2

Deduplicate before assignment

What I check: contact and company search runs before any owner is resolved.

Evidence to collect: match results by email, phone, company domain, and open-deal lookup.

Pass condition: one human, one contact record, one owner. Existing matches update instead of create.

Common failure: route first, dedupe later. Two reps receive the same buyer and the pipeline splits.

STEP 3

Validate owner and pipeline state

What I check: the target owner exists, is active, holds the right team and territory, and can receive this source.

Evidence to collect: owner status response, pipeline existence, stage existence, open-deal check.

Pass condition: target_owner_id resolves to a live user and target_pipeline / target_stage both exist.

Common failure: a user left and the CRM kept the ID alive. Leads land in an inbox nobody reads.

STEP 4

Write to CRM, then the routing reason

What I check: owner, pipeline, stage and the reason note all land in one confirmed write sequence.

Evidence to collect: API response codes, before and after values, the stored routing_reason.

Pass condition: the CRM is usable before anyone is notified. The record is reconstructable from its event_id.

Common failure: renamed property, expired token, or a retry that creates a second record instead of updating.

STEP 5

Alert after the write confirms

What I check: the owner alert, the follow-up sequence and any SLA timer start only after CRM success.

Evidence to collect: alert timestamp versus CRM write timestamp, sequence enrollment id.

Pass condition: the rep receives a notification only when the CRM state is usable, with context in the message itself.

Common failure: Slack fires before HubSpot finishes. The rep opens a record that does not exist yet.

Table 4A · Owner and pipeline validation matrix

CheckPassOn failureWhere it is logged
Owner exists in CRMfallback owner then reviewfailure queue
Owner is active
Owner belongs to target team
Owner handles this territory
Owner accepts this source
Pipeline exists
Stage exists and fits status
Open-deal duplication check passed

Table 4B · CRM execution order

Run these in sequence. No step may start before the previous one confirms.

OrderActionObject touchedConfirmed by
1Store raw lead eventevent storewrite acknowledged
2Normalize payloadcontract objectschema valid
3Deduplicate contact and companycontact, companysearch result returned
4Search open dealsdealsearch result returned
5Create routing decisiondecision objectdecision stored
6Update CRM owner and pipelinecontact, dealAPI 200 + object ids
7Write routing reasonnote or propertyAPI 200
8Start follow-up sequencesequence enrollmentenrollment id returned
9Send owner alertSlack or emailsteps 6 to 8 all green
Non-negotiable order

Always deduplicate before assignment, never after. Dedupe after assignment creates ownership conflict: one rep gets the first record, another gets the duplicate, the buyer receives two follow-ups. The idempotency key in Section 5 is what makes a retry safe.

06

Section 5 · Failure queue, routing reason, audit trail

A failed route should never disappear. It lands in a review queue with the raw payload and the decision attempt. The error reason and a replay option sit beside them. This is the section that turns "the lead went missing" into a five-minute lookup.

Table 5A · Failure queue rules

TriggerQueue actionIdempotency behaviourHuman step
Missing email and phonemanual_reviewsame key, no createoperator enriches or rejects
Ambiguous duplicate candidatesmanual_reviewsame key, no createoperator merges or splits
Inactive or unknown owner
Pipeline or stage missing
Invalid territory
Low-confidence classification
Webhook retry conflict
CRM API error after max attempts

Idempotency key format

One key per source event. The key is what makes a replay safe: same key, same decision, no second assignment. Compute it at the edge and thread it through every write.

idempotency_key = source_system + ":" + source_record_id + ":" + normalized_email Example: lead_route:web_form:submission_8841:ops@example.com Replay rule: if idempotency_key already assigned -> return existing owner_id, write nothing if idempotency_key in flight -> return 409 to the provider, let it retry if idempotency_key unknown -> run the full path from Step 1

Table 5B · Audit trail fields (what every run stores)

FieldContainsWhy a sales manager asks for it
event_idOne source event, one id"Show me the exact submission."
correlation_idThreads the event across systems"Where did this lead stop?"
idempotency_keyDedupe and replay handle"Why is this contact here twice?"
routing_reasonHuman sentence behind the assignment"Why did this go to Elena?"

Failure policy block

failure_policy: missing_contact_data -> manual_review ambiguous_duplicate -> manual_review inactive_owner -> fallback_owner_then_review pipeline_missing -> stop_and_alert_operator crm_api_rate_limit -> retry_with_backoff invalid_ai_output -> manual_review max_attempts -> 5 dead_letter_queue -> lead_routing_failed_events observability: store_raw_payload true store_normalized_lead true store_routing_decision true store_crm_object_ids true store_owner_assignment_reason true enable_safe_replay true

Replay and review checklist

  • Every failure lands somewhere visible: raw payload, decision attempt, error reason, named queue owner.
  • Replay is idempotent: re-running the same idempotency_key never creates a second owner assignment.
  • Max attempts are bounded: a stuck event moves to the dead-letter queue instead of retrying forever.
  • Rate limits slow the queue: CRM 429 responses back off instead of dropping payloads.
  • Manual review has an SLA: a human reads the queue on a schedule.
  • The routing reason is stored with the record: sales can read why the lead landed there without asking ops.
What Meridian proved

The firm now runs twelve intake channels across three offices, and leads get an owner within ten seconds of submission. Every assignment is inspectable through the audit trail. The SLA timers that did not exist now escalate on their own. The rebuild was the routing layer alone.

07

Where this kit fits

Start here if the pipeline does not exist yet: the Speed-to-Lead Blueprint Pack is the entry blueprint. It covers capture, validation, dedupe, enrichment, scoring, routing and assignment as one pipeline. This kit is the routing layer written out in full.

If duplicates are the real problem: reach for the CRM Automation Repair Kit. Merging duplicate contacts, fixing associations and correcting field maps all sit under routing. Fix the data layer before you add more rules.

For the production build: Fix your pipeline is the engagement that ships the scoring model, the routing layer and the failure queue as one system. The proof is the Meridian Legal Group case study: twelve intake channels, three offices, assignment inside ten seconds.

08

Section 6 · FAQ

Do I need to rebuild the whole CRM to use this?

No. The kit is the routing layer. It carries the contract, the hierarchy and the decision object, plus the validation gates and the failure queue. It sits in front of whatever CRM you already run. The CRM stays. The ownership logic changes.

What if my form sends only part of the contract?

Then that source goes through the failure path for those fields, while the router stays on its rules. Table 1B is where you record the gaps. A short diagnostic intake with extraction behind a strict schema is usually the cleaner fix than a longer form.

Where does dedupe belong in the chain?

Before assignment, always. Search contact by email and phone, company by domain, and look for open deals before any owner resolves. Dedupe after assignment creates two owners on one buyer and splits the pipeline.

Can Make, Zapier and n8n all execute this decision object?

Yes. That is the point of writing the decision once. The executor reads target_owner_id, target_pipeline, target_stage and routing_reason and performs the writes. Swap the executor without rewriting a single rule.

What happens when routing finds no owner to validate?

The lead goes to the manual review queue with its raw payload, decision attempt and error reason. Every lead keeps its place in the queue with its payload attached. Unresolved is a state with an owner and a queue behind it.

Can you build this for my firm instead?

Yes. The kit stands alone. The same routing layer ships as a client engagement through Fix your pipeline, with the scoring model and SLA timers included.

What happens next. Run the free workflow diagnostic, or read the Meridian Legal Group case study, then decide. When you want the build done for you, the same systems ship as client engagements: routing rebuilds, lead qualification, CRM and API overhaul, and monitored production workflows.

09

Get the fillable workbook

Everything above is yours to read. The workbook adds the empty worksheets, so you fill them in on your own stack with your own owners, fields and failures. It opens in any browser, on any device, and it saves as you type.