Overview
Core ownership, trigger, and runtime details for the Contact MQL routing process.
| Field | Value |
|---|---|
| Process Name | Lead Routing Engine - Contact MQL Routing |
| Business Owner | RevOps |
| Technical Owner | Salesforce Admin / Salesforce Engineering |
| Primary Object | Contact |
| Trigger Event | Contact.lifecyclestage__c changes to Marketing Qualified Lead |
| Runtime Pattern | Record-triggered Flow detects the event; Apex classifies and assigns; Flow performs Sales Engagement cadence enrollment where applicable. |
Purpose and Scope
The Lead Routing Engine assigns ownership for Contacts that become Marketing Qualified Leads. It is an Account-based routing process: Account ownership, Account classification, active deals, Account BDR ownership, and territory roster data determine the Contact owner.
MQL Contact routing
Route a Contact to the correct AE or BDR when HubSpot syncs the lifecycle stage to Marketing Qualified Lead.
Visible failure handling
Create Routing_Exception__c records for every unrouted outcome so RevOps can triage and resolve issues.
Routing auditability
Stamp routing path, last run time, speed-to-route metric, and last error detail on Contact records.
Lead object routing
Inbound records route as Contacts. Salesforce Lead assignment rules are not part of this process.
Account lifecycle automation
Pool-claim automation, automatic BDR sync after Region correction, and maintenance sweeps are deferred.
Marketing email tooling
Instantly and other marketing email processes do not participate in Salesforce routing decisions.
Actors and Systems
Systems and teams that invoke, execute, configure, or consume routing outcomes.
| Actor or System | Responsibility | Salesforce process impact |
|---|---|---|
| HubSpot | System of record for Contact lifecycle stage, re-MQL timing, and Contact-to-Account matching. | Routing starts only after HubSpot or an authorized user updates lifecyclestage__c to Marketing Qualified Lead. |
| Integration or sync user | Writes Contact updates into Salesforce. | Must hold Routing_Trigger_Users to pass the default-deny routing gate. |
| Salesforce Flow | Detects the trigger event, gates execution, calls Apex, and enrolls BDR-routed Contacts in cadence. | Flow is intentionally thin. It does not classify routing paths. |
| Apex routing service | Classifies path, writes ownership changes, inserts exceptions, publishes events, and isolates per-Contact failures. | ContactRouter and RotationService are the only production Apex classes in this process. |
| RevOps | Maintains roster/configuration and resolves routing exceptions. | Uses Custom Metadata, Custom Settings, and queue/report views for ongoing operations. |
| Clay | Consumes routed-contact events. | Subscribes to Contact_Routed__e after successful routed paths. |
| Sales Engagement | Runs the non-Target cadence for BDR-routed Contacts. | Requires active cadence configuration and user permissions/license readiness. |
Prerequisites
These conditions must be true before this process can run predictably in any sandbox or production org.
- Routing Flows are active:
Contact_BeforeSave_StampMQLandContact_AfterSave_RouteOnMQL. - Integration user has trigger permission: assign
Routing_Trigger_Usersto users whose Contact updates should invoke routing. - Bypass permission is controlled:
Routing_Bypass_Allis assigned only when intentionally pausing automation. - Runtime user can update routing fields: assign
Routing_Engine_Servicewhere required for field access and Account owner transfer bypass. - Configuration exists: create
Routing_Config__mdt.Defaultwith Prospecting Pool user Id, triage owner Id, and non-Target cadence Id if cadence is enabled. - Roster exists: maintain
Rep_Roster__mdtrows for active AEs and BDRs by region and segment. - Territory fields are populated: rotation paths require Account
Region__cand AE rotation also requiresRevenue_Range_Segments__c. - Sales Engagement is ready: cadence is active and the Flow-running user has the required Sales Engagement access.
Process Steps
This is the process flow at business level. Technical implementation details are documented in later sections.
MQL event occurs
Contact lifecycle changes to Marketing Qualified Lead. Before-save Flow stamps MQL timestamp and status.
Execution is gated
After-save Flow checks trigger permission, then bypass permission. Failed gate means clean exit.
Apex classifies
ContactRouter.route evaluates gates and routing paths in a fixed first-match-wins order.
Ownership updates
Contact owner changes to AE or BDR. For new-account rotation, Account owner changes to AE and Account BDR is stamped.
Side effects run
Successful paths publish a routing event. BDR-routed paths enter cadence. Tagged target Accounts can create a manual-outreach Task.
Admins monitor
Admins review Contact routing fields and open routing exception records for failed outcomes.
Invocation path: this process is designed for record-triggered execution and bulk updates. It handles multiple Contacts in a transaction and preserves buying-group continuity for Contacts sharing the same Account in the same batch.
Decision Matrix
Routing paths are evaluated from top to bottom. The first matching condition determines the outcome.
flowchart TD
Start(["Contact becomes Marketing Qualified Lead"]) --> TriggerGate{"Trigger_Routing permission?"}
TriggerGate -- "No" --> ExitNoRoute(["Exit without routing"])
TriggerGate -- "Yes" --> BypassGate{"Bypass_Automation permission?"}
BypassGate -- "Yes" --> ExitBypass(["Exit without routing"])
BypassGate -- "No" --> HoldGate{"On hold, do not route, or disqualified?"}
HoldGate -- "Yes" --> Hold(["HOLD"])
HoldGate -- "No" --> DataGate{"Email and Account present?"}
DataGate -- "No" --> BadData(["UNROUTED_BAD_DATA"])
DataGate -- "Yes" --> TaggedGate{"Tagged Account?"}
TaggedGate -- "Yes" --> TaggedOwner{"Active non-Pool Account owner?"}
TaggedOwner -- "Yes" --> Tagged(["TAGGED_ACCOUNT"])
TaggedOwner -- "No" --> TaggedNoOwner(["UNROUTED_TAGGED_NO_OWNER"])
TaggedGate -- "No" --> CustomerGate{"Customer Account?"}
CustomerGate -- "Yes" --> CustomerOwner{"Active non-Pool Account owner?"}
CustomerOwner -- "Yes" --> Customer(["CUSTOMER_ACCOUNT"])
CustomerOwner -- "No" --> NoOwner(["UNROUTED_NO_OWNER"])
CustomerGate -- "No" --> DealGate{"Open Opportunity with active non-Pool owner?"}
DealGate -- "Yes" --> ActiveDeal(["ACTIVE_DEAL"])
DealGate -- "No" --> AccountBdrGate{"Active Account BDR?"}
AccountBdrGate -- "Yes" --> AccountBdr(["ACCOUNT_BDR"])
AccountBdrGate -- "No" --> AccountOwnerGate{"Account owner is Pool or inactive?"}
AccountOwnerGate -- "Yes" --> RegionGateNew{"Account Region populated?"}
RegionGateNew -- "No" --> NoRegion(["UNROUTED_NO_REGION"])
RegionGateNew -- "Yes" --> AeGate{"Eligible AE in roster?"}
AeGate -- "No" --> NoAe(["UNROUTED_NO_AE"])
AeGate -- "Yes" --> BdrGateNew{"Eligible BDR in roster?"}
BdrGateNew -- "No" --> NoBdr(["UNROUTED_NO_BDR"])
BdrGateNew -- "Yes" --> RotationNew(["ROTATION_NEW_ACCOUNT"])
AccountOwnerGate -- "No" --> RegionGateBdr{"Account Region populated?"}
RegionGateBdr -- "No" --> NoRegion
RegionGateBdr -- "Yes" --> BdrGateOnly{"Eligible BDR in roster?"}
BdrGateOnly -- "No" --> NoBdr
BdrGateOnly -- "Yes" --> RotationBdr(["ROTATION_BDR_ONLY"])
classDef entry fill:#15161a,stroke:#15161a,color:#def862,font-weight:bold;
classDef routed fill:#e5f4eb,stroke:#246f49,color:#18191d,font-weight:bold;
classDef hold fill:#fff3d9,stroke:#9b6400,color:#18191d,font-weight:bold;
classDef unrouted fill:#ffe8e5,stroke:#b42318,color:#18191d,font-weight:bold;
classDef exit fill:#eef0f2,stroke:#5b6270,color:#18191d;
class Start entry;
class Hold hold;
class Tagged,Customer,ActiveDeal,AccountBdr,RotationNew,RotationBdr routed;
class BadData,TaggedNoOwner,NoOwner,NoRegion,NoAe,NoBdr unrouted;
class ExitNoRoute,ExitBypass exit;
| Order | Outcome | Condition | Action | Cadence / Task |
|---|---|---|---|---|
| 0 | HOLD |
Contact on hold, Account do-not-route, or Contact status is Disqualified. | Stamp path and leave ownership unchanged. | No cadence. No exception. |
| 0 | UNROUTED_BAD_DATA |
Email or Account is missing. | Create Routing Exception and leave ownership unchanged. | No cadence. |
| 1 | TAGGED_ACCOUNT |
Account is Active Target, Active Conversion, or Active Velocity. | Route Contact to active Account owner. | Active Target with Account BDR creates manual-outreach Task. No cadence. |
| 2 | CUSTOMER_ACCOUNT |
Account status is Customer. | Route Contact to active non-Pool Account owner. Broken ownership becomes UNROUTED_NO_OWNER. |
No cadence. |
| 3 | ACTIVE_DEAL |
Account has newest open Opportunity with active non-Pool owner. | Route Contact to Opportunity owner. | No cadence. |
| 4 | ACCOUNT_BDR |
Account has active BDR_Owner__c. |
Route Contact to Account BDR. | Enroll in non-Target cadence. |
| 5 | ROTATION_NEW_ACCOUNT |
Account owner is Prospecting Pool or inactive, and eligible AE and BDR exist. | Rotate AE to Account, rotate BDR to Contact, stamp Account BDR. | Enroll in non-Target cadence. |
| 6 | ROTATION_BDR_ONLY |
Account has active AE but no usable BDR pointer. | Preserve Account owner, rotate BDR to Contact, stamp Account BDR. | Enroll in non-Target cadence. |
Exception Handling
The process fails visibly. Every unrouted outcome inserts a Routing_Exception__c record for RevOps triage and leaves Contact ownership unchanged.
UNROUTED_BAD_DATA
Missing Email or Account. Fix source sync, enrichment, or Account matching.
UNROUTED_TAGGED_NO_OWNER
Tagged Account has no active non-Pool owner.
UNROUTED_NO_OWNER
Customer Account has broken ownership. It does not fall through to rotation.
UNROUTED_NO_REGION
Rotation was needed but Account Region was blank.
UNROUTED_NO_AE
No eligible AE for the Account region and segment.
UNROUTED_NO_BDR
No eligible BDR for the Account region.
UNROUTED_ERROR
Apex or DML failed for that Contact. Other Contacts in the same transaction can still route.
Resolution notes
Admins update exception status and notes directly on Routing_Exception__c.
Salesforce Components
This section documents the automation and metadata components admins and developers maintain.
| Component | Type | Purpose | Maintenance owner |
|---|---|---|---|
Contact_BeforeSave_StampMQL |
Record-triggered Flow | Stamps MQL timestamp and Contact status on lifecycle flip. | Salesforce Admin |
Contact_AfterSave_RouteOnMQL |
Record-triggered Flow | Gates execution, invokes Apex, and handles BDR cadence enrollment. | Salesforce Admin / Engineering |
ContactRouter |
Apex class | Classifies paths, writes ownership, stamps audit fields, creates Tasks/exceptions, publishes events. | Salesforce Engineering |
RotationService |
Apex class | Performs locked round-robin selection using Rep_Rotation_Counter__c. |
Salesforce Engineering |
Routing_Exception__c |
Custom object | Operational triage queue for failed routing attempts. | RevOps |
Rep_Roster__mdt |
Custom Metadata Type | Configures eligible AEs and BDRs by role, region, segment, active status, and out-of-office date. | RevOps / Salesforce Admin |
Routing_Config__mdt |
Custom Metadata Type | Stores environment-specific Prospecting Pool, triage owner, and non-Target cadence Ids. | Salesforce Admin |
Contact_Routed__e |
Platform Event | Publishes successful routed outcomes for downstream consumers. | Salesforce Engineering / Integrations |
Metadata Component Inventory
This is the full routing metadata inventory represented in source, including child metadata such as fields. The deployment manifest lists 33 top-level members; this expanded inventory counts 60 metadata components.
| Metadata type | Components created |
|---|---|
| ApexClass | ContactRouter, ContactRouterTest, RotationService, RotationServiceTest |
| Flow | Contact_AfterSave_RouteOnMQL, Contact_BeforeSave_StampMQL |
| CustomPermission | Bypass_Automation, Transfer_Account_Ownership, Trigger_Routing |
| PermissionSet | Routing_Bypass_All, Routing_Engine_Service, Routing_Trigger_Users |
| CustomObject / Custom Metadata / Platform Event | Contact_Routed__e, Rep_Roster__mdt, Rep_Rotation_Counter__c, Routing_Config__mdt, Routing_Exception__c |
| CustomField on Account | Account.Account_Status__c, Account.BDR_Owner__c, Account.Do_Not_Route__c, Account.Region__c, Account.Revenue_Range_Segments__c, Account.Target_Account_Status__c |
| CustomField on Contact | Contact.Contact_Status__c, Contact.Disqualification_Reason__c, Contact.MQL_Stamped_At__c, Contact.On_Hold__c, Contact.Routing_Last_Error__c, Contact.Routing_Last_Run__c, Contact.Routing_Path__c, Contact.Time_To_Route_Seconds__c, Contact.lifecyclestage__c |
| CustomField on Contact_Routed__e | Contact_Routed__e.Account_Owner__c, Contact_Routed__e.Assigned_BDR__c, Contact_Routed__e.Contact_Id__c, Contact_Routed__e.Region__c, Contact_Routed__e.Routing_Path__c, Contact_Routed__e.Routing_Timestamp__c |
| CustomField on Rep_Roster__mdt | Rep_Roster__mdt.Is_Active__c, Rep_Roster__mdt.Out_Until__c, Rep_Roster__mdt.Paired_BDR__c, Rep_Roster__mdt.Region__c, Rep_Roster__mdt.Role__c, Rep_Roster__mdt.Segment__c, Rep_Roster__mdt.User__c |
| CustomField on Rep_Rotation_Counter__c | Rep_Rotation_Counter__c.Region__c, Rep_Rotation_Counter__c.Role__c, Rep_Rotation_Counter__c.Rotation_Counter__c, Rep_Rotation_Counter__c.Segment__c |
| CustomField on Routing_Config__mdt | Routing_Config__mdt.NonTarget_Cadence_Id__c, Routing_Config__mdt.Prospecting_Pool_User_Id__c, Routing_Config__mdt.Unrouted_Triage_Owner_Id__c |
| CustomField on Routing_Exception__c | Routing_Exception__c.Account__c, Routing_Exception__c.Contact__c, Routing_Exception__c.Error_Detail__c, Routing_Exception__c.Failure_Reason__c, Routing_Exception__c.Resolution_Notes__c, Routing_Exception__c.Routed_At__c, Routing_Exception__c.Status__c |
| Queue | Routing_Triage |
Not included as created metadata: org-specific records such as Routing_Config__mdt.Default, real Rep_Roster__mdt rows, rotation counter rows, users, queues beyond Routing_Triage, and Sales Engagement cadence records are configured per environment.
Data Model and Field Usage
Only fields that drive or document this process are listed here. See docs/objects-and-fields.md for the full field inventory.
Trigger and audit fields
lifecyclestage__c, Contact_Status__c, On_Hold__c, Routing_Path__c, Routing_Last_Run__c, Routing_Last_Error__c, MQL_Stamped_At__c, Time_To_Route_Seconds__c.
Path input fields
OwnerId, BDR_Owner__c, Region__c, Revenue_Range_Segments__c, Account_Status__c, Do_Not_Route__c, Target_Account_Status__c.
Triage fields
Contact__c, Account__c, Failure_Reason__c, Status__c, Error_Detail__c, Resolution_Notes__c, Routed_At__c.
Configuration data is environment-specific: Routing_Config__mdt.Default and real roster rows can contain User, Queue, and Cadence Ids. Do not treat those records as portable metadata unless the release process explicitly manages per-environment values.
Controls and Safeguards
Controls admins use to stop, resume, and protect routing execution.
Trigger_Routing
Default-deny gate. Users without this permission do not invoke routing when they save Contacts.
Bypass_Automation
Emergency bypass. Users with this permission exit routing immediately after the trigger gate.
Transfer_Account_Ownership
Allows the routing runtime user to pass Account owner-change validation rules when assigning AEs to new Accounts.
Bulkified Apex
SOQL and DML are collected outside per-Contact loops. Rotation counters are locked with row-level locking.
Per-Contact try/catch
One failed Contact becomes UNROUTED_ERROR without rolling back other Contacts in the batch.
Field stamps and triage object
Routing state is visible on Contact and failed attempts are visible in a dedicated object.
Admin Automation Boundary
Routing truth stays in Apex. Flow is used only where admins need an operational workflow or where Salesforce exposes the required action only in Flow.
| Area | Owner | Reason |
|---|---|---|
| Path classification, ownership writes, rotation, exceptions, events, and tagged-account outreach Tasks | Apex | Transaction-critical, bulk-sensitive behavior with unit test coverage. |
| MQL stamping, trigger/bypass gates, and cadence enrollment | Flow | Simple event orchestration, before-save stamping, and the Flow-only Sales Engagement action. |
| Future exception notifications, aging, and SLA reminders | Flow after RevOps defines rules | Admin-maintained operational behavior; not shipped as placeholder automation without owner, timing, channel, and duplicate-prevention requirements. |
Deprecated cleanup: Contact.BDR_Owner__c and the old Contact_After_Save_Create_Call_Sequence Flow are retired. Use Account.BDR_Owner__c for BDR continuity, standard Contact.OwnerId for the routed Contact owner, and Contact_AfterSave_RouteOnMQL for cadence enrollment.
Monitoring and KPIs
Healthy automation should be tied to measurable outcomes. These are the recommended operational signals for routing.
| Metric or Signal | Where to measure | Expected use |
|---|---|---|
| Time to route | Contact Time_To_Route_Seconds__c |
Primary speed-to-lead SLA metric. Trend by source, segment, and owner. |
| Routing volume by path | Contact report grouped by Routing_Path__c |
Detect unexpected spikes in hold, rotation, or unrouted outcomes. |
| Open exceptions | Routing_Exception__c queue/report view |
Daily RevOps work queue. SLA target: no aged open exceptions beyond the agreed threshold. |
| Cadence failures | Contact Routing_Last_Error__c |
Identify Sales Engagement permission, cadence, or target data issues. |
| Rotation distribution | Contact report grouped by owner, region, segment, and path | Weekly review for fairness within each active rotation cohort. |
| Downstream event delivery | Contact_Routed__e subscriber monitoring |
Confirm Clay receives successful routed outcomes when integration testing is in scope. |
Routine Maintenance
These tasks keep the process current without changing routing code.
Daily exception triage
- Open the configured
Routing_Exception__cqueue or report view. - Review open and investigating exceptions.
- Group by
Failure_Reason__cto identify common root causes. - Fix the source data or configuration before re-routing affected Contacts.
- Set status to Resolved only after the root cause is fixed.
Weekly routing distribution review
- Report on Contacts routed in the last 7 days.
- Group by
Routing_Path__c, owner, Account region, and Account segment. - Review rotation cohorts for material skew.
- Check roster rows and out-of-office exclusions before editing counters.
Monthly roster reconciliation
- Compare active roster rows to current AE and BDR headcount.
- Deactivate rows for departed reps instead of deleting them.
- Clear expired
Out_Until__cvalues. - Confirm User Ids point to active Salesforce users.
Configuration review
- Confirm Prospecting Pool user is active.
- Confirm triage owner queue/user is active and monitored.
- Confirm cadence Id points to the intended active cadence.
- Confirm permission assignments match the current integration user model.
Change Management
Changes should be classified by whether they are admin configuration, controlled data operations, or release-managed metadata changes.
| Change type | Allowed path | Validation required |
|---|---|---|
| Add or deactivate a rep | Admin configuration in Rep_Roster__mdt. |
Route a test MQL in the affected region/segment. |
| Change cadence, triage owner, or Prospecting Pool | Admin configuration in Routing_Config__mdt.Default. |
Run a BDR-path smoke test and a forced exception test. |
| Pause or resume routing | Permission assignment change using Routing_Trigger_Users or Routing_Bypass_All. |
Confirm a test Contact either does or does not stamp Routing_Last_Run__c as expected. |
| Bulk re-route after root-cause fix | Approved admin/developer script invoking ContactRouter.route. |
Sample routed Contacts and confirm no repeated exceptions. |
| Change routing path order or conditions | Release-managed Apex change. | Update specs first, run Apex tests, validate in sandbox, and document affected paths. |
| Add new Region or Segment | Coordinated metadata/configuration change. | Align Account picklists, roster rows, and smoke tests for each new cohort. |
Change rule: update this page when behavior, ownership, prerequisites, data dependencies, or monitoring responsibilities change. Do not rely on code comments or Flow element names as the only reference.