Free kit / Routing Stack
Routing Stack
Two blueprints in one file. The seven stage speed-to-lead pipeline and the routing layer written out in full, so the pipeline and the layer that decides who gets it are in the same document and use the same event contract.
This stack bundles two kits: Speed-to-Lead Blueprint for the pipeline, and Automated Lead Routing Kit for the layer that decides who owns the lead. If you are not sure which one is broken, run the Workflow Audit Kit first.
Part 1 · Speed-to-Lead Blueprint Pack
Seven stages, one pipeline: capture to assigned rep in under ten seconds, with the config and the failure behaviour written down.
Every form fill, ad conversion and chat message goes through the same seven stages: validation, dedupe, enrichment, scoring, routing and assignment. This pack is the buildable version of that pipeline. Stage by stage, with the keys you set and the way each stage fails.
The pipeline
One unified webhook endpoint accepts form fills, chat transcripts and ad conversions. Payloads get normalized and schema-checked before anything else happens. From there the lead moves left to right, then writes back and notifies.
ingress
|
v
[01 Capture] -> [02 Validation] -> [03 Dedupe] -> [04 Enrich] -> [05 Scoring]
|
v
[07 Assign] <- [06 Router] <- -----------------------------------+
| ^
| +---- conflict resolver (score or territory tie)
v
[CRM write] -> [Notifier: Slack / SMS / Email] -> [Dashboard readback]
dead-letter queue + correlation log on every hop| Stage | Name | Job | Tells you when it is wrong |
|---|---|---|---|
| 01 | Capture | Accept one event shape from every source | Event count drops or payload shape changes |
| 02 | Validation | Reject garbage before it becomes a record | Reject rate climbs above baseline |
| 03 | Dedupe | Match before create, merge with a paper trail | New record count spikes against stable traffic |
| 04 | Enrich | Firmographic and intent context in parallel | Provider timeouts, cache hit rate falls |
| 05 | Scoring | One fit and behaviour score, 0 to 100 | Score distribution shifts without a traffic change |
| 06 | Router | Territory, load and availability in one pass | Unresolved owners, territory conflicts queued |
| 07 | Assign | Hand to a rep or queue, start the SLA clock | Notification not acknowledged inside the window |
Stage-by-stage build guide
Capture gateway
Inputs: website and landing page forms, webinar and content downloads, Facebook Lead Ads, Google Ads conversions, LinkedIn Lead Gen Forms, Drift or Intercom chat transcripts.
Config: one public endpoint. Normalize every source into your internal event contract before anything downstream reads it. Preserve event_id, correlation_id, idempotency_key, source_system, source_record_id, utm_source, utm_campaign. Store the raw payload alongside the normalized one.
Guardrail: HMAC signature verification on the endpoint before processing. Unauthorized test submissions are a real source of junk records.
Failure behaviour: non-2xx to the provider, payload persisted for replay, alert if the error rate moves. Budget 150 ms.
Validation
Inputs: the normalized event.
Config: schema and sanity checks. Required fields present. Email format valid. Consent captured where regulation needs it. Enum fields limited to allowed values. Budget and urgency checked against the list you route on. Session and UTM context passes through untouched.
Rule: reject to a review queue, do not write a partial record. A contact created with an empty required field is worse than no contact, because the follow-up sequence will fire on stale data.
Failure behaviour: reject count and reasons logged per field, dead-letter queue holds the payload. Budget 100 ms.
Dedupe (Redis + fuzzy)
Inputs: validated event.
Config: deterministic match on email and phone. Fuzzy match on company name and normalized domain. Redis keyed on normalized values for the lookup, target under 100 ms. Merge strategy per field: first non-empty wins for contact fields, latest wins for behavioural fields, never overwrite a human correction.
Order of operations: search contact by email, search company by domain, search open deals by contact and pipeline, update when found, create only after the search clears. Store every object ID against the original event.
Failure behaviour: ties and near-matches go to the conflict resolver, never auto-merged. Every merge logged with its inputs. Budget 100 ms.
Enrichment
Inputs: deduped identity (email, domain, company name).
Config: parallel lookups against providers such as Clearbit and Apollo, plus LinkedIn where your licence allows it. Cache keyed on domain with a TTL you can defend. Warm the cache on your top 200 domains before cutover.
Rule: a lead never waits on a lookup. Hard timeout per provider, then fall through to cached data and mark the record enrichment: partial.
Failure behaviour: circuit breaker per provider, staggered re-enable, alert when the cache hit rate falls below your floor. Budget 3000 ms including timeout.
Scoring
Inputs: firmographic fit, behavioural interest, source, campaign, prior interaction history.
Config: one 0 to 100 score. Keep the inputs visible on the record: fit component, behaviour component, and the rules that fired. Retrain on conversion outcomes so routing decisions improve as the dataset grows.
Rule: if a model classifies messy text, force it to choose from a closed list and validate the choice: sales_priority, sales_standard, founder_review, nurture, reject_spam, manual_review_missing_data. Store the raw answer beside the summary. Raw pain tells you what the buyer said. Summaries compress, and compression loses edge.
Failure behaviour: score outside 0 to 100 or missing inputs routes to manual_review_missing_data. Budget 400 ms.
Router
Inputs: score, territory, availability, existing owner, open deal status, customer status.
Config: territory rules by region, account book, segment or language. Load balancing across active reps. Account-based routing, round-robin and skill-based assignment run through the same engine so there is one place to inspect.
Rule: owner status is checked at routing time. The record cannot be trusted for it. An inactive owner ID is the fastest way to lose a lead. Log the decision: which rule, which evidence, which rep.
Failure behaviour: unresolved territory or tie goes to the conflict resolver, then to a manual queue with an SLA timer. The router keeps a dead-letter queue for anything it cannot place. Budget 250 ms.
Assign and notify
Inputs: resolved owner, lead record, context note.
Config: write to CRM, then fire the notification hub: Slack, SMS, push and email with priority queuing. Include enough context in the alert that the rep does not open the CRM to decide whether to care.
SLA escalation: if the primary rep does not acknowledge inside the window, escalate to the next rep automatically, then to the team channel. Timer state lives with the lead so a restart does not lose it.
Failure behaviour: notification failure does not roll back the assignment, it pages. Budget 300 ms assign, 400 ms notify.
Make.com scenario outline
Plain module sequence. Treat it as the shape of the build. The config above is what you copy into it. Put error handlers on every module that writes.
1 Webhook (custom trigger) receives raw payload
2 HTTP: verify HMAC signature reject unsigned calls
3 Tools: set variable event_id, correlation_id, received_at
4 JSON: parse + schema validate required fields, types, enums
5 Router A: valid / invalid
5a Data store: append rejected dead-letter record with reason
5b Router B: match or new
6 Search: CRM contact by email
7 Search: CRM company by domain
8 Search: CRM open deals by contact
9 HTTP: Redis GET normalized key deterministic dedupe hit
10 HTTP: fuzzy score company/domain near match under threshold
11 Router C: create / update / conflict
11a HTTP: Clearbit enrich parallel, hard timeout
11b HTTP: Apollo enrich parallel, hard timeout
11c Tools: merge cached fallback
11d HTTP: scoring service 0 to 100 + routing label
11e Router D: territory + load + owner status
11f HTTP: conflict resolver ties and high-value leads
12 HTTP: CRM create or update idempotency key header
13 HTTP: CRM associate objects contact, company, deal, note
14 Data store: append audit row before, after, rule, owner
15 Slack: priority alert to rep
16 SMS / email: fallback channel
17 Error handler (attached to 4,9,11a,11b,11d,12,15)
-> HTTP: alert on-call
-> Data store: append failure with correlation_id
-> route to DLQ for replayPut a token bucket in front of the CRM. Bulk operations against a 100 requests per minute ceiling will throw 429s the moment you go live with real volume. Handle it with concurrency, exponential backoff and jitter instead of a plain retry loop.
Example webhook payloads
Inbound capture (source to your endpoint)
{
"event_id": "sub_01HYT9C7M4",
"source_system": "tally",
"source_record_id": "audit_request",
"received_at": "2026-04-26T10:17:33.884Z",
"email": "ops@example.com",
"phone": "+39 051 000 0000",
"full_name": "Elena Moretti",
"company_name": "Example Srl",
"company_domain": "example.com",
"budget_range": "5000_15000",
"urgency": "high",
"pain_summary": "Form submissions create duplicate contacts and never route to an owner.",
"consent_to_contact": true,
"utm_source": "linkedin",
"utm_campaign": "revops_q2",
"raw_payload_stored": true
}Normalized internal contract (every source maps here)
{
"event_id": "sub_01HYT9C7M4",
"correlation_id": "corr_lead_01HYT9C7M4",
"idempotency_key": "lead_intake:tally:audit_request:sub_01HYT9C7M4:ops@example.com",
"source_system": "tally",
"source_record_id": "audit_request",
"email": "ops@example.com",
"phone": "+39 051 000 0000",
"company_domain": "example.com",
"budget_range": "5000_15000",
"urgency": "high",
"pain_summary": "Form submissions create duplicate contacts and never route to an owner.",
"consent_to_contact": true,
"utm_source": "linkedin",
"utm_campaign": "revops_q2"
}Post-stage record (what the CRM receives)
{
"correlation_id": "corr_lead_01HYT9C7M4",
"dedupe": {
"match_type": "deterministic_email",
"existing_contact_id": "123456789",
"action": "update_existing"
},
"enrichment": {
"status": "cached",
"sources": ["clearbit", "apollo"],
"employees": 120,
"industry": "software"
},
"score": { "total": 82, "fit": 74, "behaviour": 90 },
"routing": {
"label": "sales_priority",
"rule": "budget_5000_15000_and_urgency_high",
"owner_id": "987654321",
"owner_status": "active",
"territory": "italy_north"
},
"controls": {
"queue": "crm_revenue_events",
"retry_policy": "exponential_backoff_with_jitter",
"dead_letter_queue": "crm_failed_workflow_events",
"replay_safe": true
}
}Latency budget
Measured per stage rather than guessed at the end. Each stage gets a hard ceiling. If a stage blows its ceiling, that is the alert.
| Stage | Budget (ms) | Measured at | On breach |
|---|---|---|---|
| Capture gateway | 150 | Endpoint entry to normalized event | Page if p95 breaches for 5 min |
| Validation | 100 | Schema check start to pass or reject | Log reject reasons, alert on rate |
| Dedupe lookup | 100 | Redis GET plus fuzzy compare | Alert on cache miss rate |
| Enrichment (with timeout) | 3000 | Parallel call start to last provider back | Circuit breaker, serve cached |
| Scoring | 400 | Inputs assembled to score returned | Fallback rule-based score |
| Router | 250 | Rules start to owner resolved | Conflict resolver, then manual queue |
| Assign | 300 | Owner resolved to CRM write ack | Retry, then DLQ |
| CRM write | 900 | API request to response | Backoff, token bucket shed |
| Notification | 400 | Write ack to provider accepted | Page on-call, do not roll back |
| Dashboard readback | 200 | Write to visible on rep view | Warn, non-blocking |
| Retry and queue allowance | 2000 | Worst case single retry cycle | Dead-letter with correlation ID |
| Typical total | 7800 | Capture to acknowledged assignment | Hard ceiling 10000 |
Headroom to the ceiling is deliberate. It absorbs one retry cycle and a cold enrichment cache without failing the promise.
Alert thresholds
Thresholds are configuration. Set them before launch, write down who they page, and review them after the first month of real traffic.
| Signal | Threshold | Window | Action |
|---|---|---|---|
| Capture endpoint error rate | > 1% | 5 minutes | Page on-call, check provider delivery |
| Schema reject rate | > 3% | 15 minutes | Alert, dump reject reasons by field |
| Capture to assign p95 | > 10000 ms | 15 minutes | Warn first, page if sustained 1 hour |
| Stage p95 over budget | > 1.5x budget | 15 minutes | Warn naming the stage |
| Enrichment provider timeout | > 10% | 15 minutes | Open circuit, serve cached, alert |
| Cache hit rate | < 60% | 1 hour | Warn, review TTL and warm list |
| Duplicate records created | > 0 per hour | 1 hour | Page, freeze creation step only |
| Unresolved owner or territory | > 0 | 15 minutes | Route to manual queue, alert ops |
| Notification unacknowledged | Primary 60 s, secondary 300 s | Per lead | Escalate up the chain automatically |
| Dead-letter queue depth | > 0 for 10 minutes | 10 minutes | Page with payload and correlation ID |
| CRM 429 responses | > 0 sustained | 5 minutes | Shed load, drop concurrency, alert |
Weekly review checklist
- Every dead-letter item from the week has an owner and a decision: replay, fix, or drop.
- Duplicate merges reviewed, each one logged with its inputs.
- Escalations counted, with the rule that fired each time.
- Scoring model inputs still match the fields you capture.
- Owner roster checked against active users in the CRM.
- Latency budget reviewed stage by stage.
Rollout plan
| Window | Build | Exit criterion |
|---|---|---|
| Weeks 1 to 2 | Instrumentation and dedupe | Baseline response times measured. Dedupe live on your highest-volume lead source. |
| Weeks 3 to 4 | Enrichment and scoring | Providers wired, caches warmed, scoring trained on your historical conversions. |
| Week 5 | Routing and notification | Territory rules, escalation chains and Slack or SMS alerts running end to end. |
| Weeks 6 to 8 | Load test and cutover | Full-volume load testing and failure drills, then every remaining source switches over. |
Build order if you cannot do it all at once
- Capture and validation. One endpoint, one contract, rejects visible.
- Dedupe. Search before create beats every downstream cleanup.
- Assignment and notification. Even a crude router beats no router.
- Failure path. Dead-letter queue, correlation log, alerts that page.
- Enrichment and scoring. Add once the boring layers are trusted.
The stages above the write path protect the record. Enrichment and scoring make the record smarter, they do not make it correct. Fix correctness first.
FAQ
Does this replace the systems blueprint?
No. The blueprint explains the architecture. This pack is the build version. It carries the keys and the module order, plus the payloads, the budgets and the thresholds.
Which orchestration tools does it assume?
Make.com for the outline, since it handles nested iterators and error handling well. Zapier, n8n and custom API receivers map to the same stage boundaries.
What if my volume is small right now?
Keep the same stage boundaries. Shrink the budgets, keep the dead-letter queue. Volume changes the sizing. The shape stays the same.
Where does AI fit?
Classifying messy pain text, urgency and fit in scoring, with a closed list of allowed outputs and validation on the choice. The final routing path still gets a strict schema.
What happens next. When the pack is not enough, the same systems ship as client engagements. The work covers speed-to-lead builds, CRM and API workflow overhaul, lead qualification systems and monitored production workflows.
Part 2 · 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.
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.
Before you start
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.
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.
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 field | Type | Required | Where it comes from | What breaks without it |
|---|---|---|---|---|
| event_id | string | yes | Generated at intake | Retries create duplicate assignments |
| correlation_id | string | yes | Generated at intake | No end-to-end trace across systems |
| idempotency_key | string | yes | source_system + source_record_id + email | Replay assigns the same lead twice |
| source_system | enum | yes | Form, ad platform, chat, import | Campaign routing has nothing to key on |
| string | yes | Form field | Dedupe finds no match, so owner resolution stops. | |
| phone | string | yes* | Form field | Second match key gone, SMS path dead |
| company_domain | string | no | Email domain or enrichment | Account matching falls back to name only |
| service_requested | enum | yes | Form select or extracted | Service-line routing has no target |
| territory | enum | yes | Form, IP, or office mapping | Leads 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 system | Required fields present | Fields it cannot send | Fallback owner / path |
|---|---|---|---|
| Tally contact form | all required | none | n/a |
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.
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
| Order | Rule | Routes to | Why it sits here |
|---|---|---|---|
| 1 | Spam filter and invalid contact data | Reject log, never the CRM | Garbage must not reach a human queue |
| 2 | Existing customer | Existing owner of record | Continuity beats distribution |
| 3 | Open deal already exists | Current deal owner | Two owners on one buyer splits the pipeline |
| 4 | Critical severity or high lead score | Priority owner or founder review | Intent beats geography |
| 5 | Territory or office location | Territory owner | Local ownership before generic pools |
| 6 | Service line or expertise | Practice or product owner | The right specialist, not the nearest rep |
| 7 | Campaign source | Campaign owner | Attribution-sensitive paths stay intact |
| 8 | Round robin pool | Next active rep in rotation | Only for genuinely unclaimed leads |
| 9 | Nothing matched | Manual review queue | Unresolved leads never disappear |
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 today | Hierarchy row | Conflicts with | Decision |
|---|---|---|---|
| If country = IT then owner_rossi | 5 territory | round robin on web form | territory 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_reasonin Section 3. - Unmatched leads have a destination: the manual review queue is rule 9 by design.
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
| Field | Allowed values | Set by | Consumed by |
|---|---|---|---|
| routing_status | assigned | review | rejected | rules engine | executor, audit log |
| routing_tier | priority | standard | review | lead score and urgency | follow-up sequence picker |
| target_owner_id | CRM user id | hierarchy row 2 to 8 | CRM owner write |
| routing_reason | human sentence | rules engine | CRM note, sales context |
| manual_review_required | true | false | failure policy | review 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 row | Reason template | Example filled |
|---|---|---|
| 2 existing customer | Existing 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 |
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.
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.
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.
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.
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.
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.
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
| Check | Pass | On failure | Where it is logged |
|---|---|---|---|
| Owner exists in CRM | fallback owner then review | failure 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.
| Order | Action | Object touched | Confirmed by |
|---|---|---|---|
| 1 | Store raw lead event | event store | write acknowledged |
| 2 | Normalize payload | contract object | schema valid |
| 3 | Deduplicate contact and company | contact, company | search result returned |
| 4 | Search open deals | deal | search result returned |
| 5 | Create routing decision | decision object | decision stored |
| 6 | Update CRM owner and pipeline | contact, deal | API 200 + object ids |
| 7 | Write routing reason | note or property | API 200 |
| 8 | Start follow-up sequence | sequence enrollment | enrollment id returned |
| 9 | Send owner alert | Slack or email | steps 6 to 8 all green |
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.
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
| Trigger | Queue action | Idempotency behaviour | Human step |
|---|---|---|---|
| Missing email and phone | manual_review | same key, no create | operator enriches or rejects |
| Ambiguous duplicate candidates | manual_review | same key, no create | operator 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 1Table 5B · Audit trail fields (what every run stores)
| Field | Contains | Why a sales manager asks for it |
|---|---|---|
| event_id | One source event, one id | "Show me the exact submission." |
| correlation_id | Threads the event across systems | "Where did this lead stop?" |
| idempotency_key | Dedupe and replay handle | "Why is this contact here twice?" |
| routing_reason | Human 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 trueReplay 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_keynever 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.
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.
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.
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.
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.