An automated audit of the go-to-market stack: CRM sync, enrichment, routing, data quality, product signals, attribution, automation and outbound. It starts with the causal chains — one root cause traced through its mechanism to the accounts it costs — then the per-system findings and a ranked list of automations worth building.
6 accounts traced through all 5 links
1. Employee count missing
37 live accounts have no employee count. The data-quality rule counts fewer, because it only raises the ones that have a domain to enrich; the rest cannot be fixed by enrichment at all.
accounts where employee_count is null and merged_into_id is null2. No segment
33 of them also have no segment, because the segment band is derived from the employee count.
… and segment is null3. No routing rule matches
8 of those reached the router in the last 90 days, and every one of them matched no rule.
… and exists (routing_decisions where outcome = 'unmatched' and decided_at >= now() - 90 days)4. No territory owner
8 ended up in the triage queue or with no owner at all, rather than with the rep whose territory they belong to.
… and (owner_id is null or the last routing decision was 'fallback_queue')5. No follow-up
6 have had no email, call, task or meeting in the last 30 days.
… and no activity of type email_sent/call/task/meeting_booked in 30 daysAccounts with no territory owner
6 accounts reached the triage queue rather than a territory owner for a reason that started with one empty field, carrying 221 points of ICP score and $0 of open pipeline.
Fix
Backfill employee count, then re-segment and re-route in the same run
Nightly: accounts with a missing core field → enrich_account → recalculate_score → route_account. Routing has to run after segmentation or the account is unmatched again.
Expected effect: 6 accounts leave the triage queue; on the median peer's company-size score, 15 of the 37 unsized accounts change grade.
How sure is thismechanism
Each link is a deterministic dependency, not a correlation: segment is computed from employee count, and the rules read segment. The counts are the intersection at every step.
What it does not prove: This chain explains only 8 of the 95 accounts unmatched in 90 days; the rest have a segment and fail elsewhere. The grade-change figure is a forecast from peer scores, not an observation.
{
"key": "size_gap_blocks_routing",
"title": "Missing employee count → no segment → no routing rule → no territory owner",
"root_cause": "Employee count missing",
"consequence_label": "Accounts with no territory owner",
"links": [
{
"step": 1,
"label": "Employee count missing",
"count": 37,
"sentence": "37 live accounts have no employee count. The data-quality rule counts fewer, because it only raises the ones that have a domain to enrich; the rest cannot be fixed by enrichment at all.",
"query": "accounts where employee_count is null and merged_into_id is null",
"evidence": {}
},
{
"step": 2,
"label": "No segment",
"count": 33,
"sentence": "33 of them also have no segment, because the segment band is derived from the employee count.",
"query": "… and segment is null",
"evidence": {
"derivation": "segment_for(employee_count): <100 smb, <500 mid_market, <2000 enterprise, else strategic"
}
},
{
"step": 3,
"label": "No routing rule matches",
"count": 8,
"sentence": "8 of those reached the router in the last 90 days, and every one of them matched no rule.",
"query": "… and exists (routing_decisions where outcome = 'unmatched' and decided_at >= now() - 90 days)",
"evidence": {
"segment_conditioned_rules": "5 of 7 active rules test account.segment",
"segmentless_accounts_ever_matched": 15,
"mechanism": "The only rule that fires without a segment is the one that does not test it (existing customer → account management)."
}
},
{
"step": 4,
"label": "No territory owner",
"count": 8,
"sentence": "8 ended up in the triage queue or with no owner at all, rather than with the rep whose territory they belong to.",
"query": "… and (owner_id is null or the last routing decision was 'fallback_queue')",
"evidence": {
"why_it_matters": "The triage queue carries the loosest first-touch SLA configured (72h) because it exists to catch what the rules missed, not to work a territory."
}
},
{
"step": 5,
"label": "No follow-up",
"count": 6,
"sentence": "6 have had no email, call, task or meeting in the last 30 days.",
"query": "… and no activity of type email_sent/call/task/meeting_booked in 30 days",
"evidence": {}
}
],
"consequence": {
"label": "Accounts with no territory owner",
"accounts": 6,
"sentence": "6 accounts reached the triage queue rather than a territory owner for a reason that started with one empty field, carrying 221 points of ICP score and $0 of open pipeline.",
"metrics": {
"icp_score_at_risk": 221,
"open_pipeline_usd": 0,
"accounts_scoring_zero_on_company_size": 37,
"median_company_size_points_for_sized_peers": 12,
"would_change_grade_if_enriched": 15
}
},
"fix": {
"title": "Backfill employee count, then re-segment and re-route in the same run",
"how": "Nightly: accounts with a missing core field → enrich_account → recalculate_score → route_account. Routing has to run after segmentation or the account is unmatched again.",
"expected_effect": "6 accounts leave the triage queue; on the median peer's company-size score, 15 of the 37 unsized accounts change grade.",
"rec_key": "enrichment_waterfall"
},
"confidence": {
"level": "high",
"basis": "mechanism",
"statement": "Each link is a deterministic dependency, not a correlation: segment is computed from employee count, and the rules read segment. The counts are the intersection at every step.",
"limits": "This chain explains only 8 of the 95 accounts unmatched in 90 days; the rest have a segment and fail elsewhere. The grade-change figure is a forecast from peer scores, not an observation."
},
"accounts": [
{
"id": "234f00c8-0d31-57d7-9874-870b744057e4",
"name": "Bolt Labs",
"icp_score": 54,
"grade": "C",
"region": "NA"
},
{
"id": "f01fc35a-67ce-54e0-9fd7-97b4b9c545da",
"name": "Glacier Stack",
"icp_score": 40,
"grade": "D",
"region": "NA"
},
{
"id": "badc0b63-fc8d-5708-991e-4280ef248410",
"name": "Basalt Works",
"icp_score": 38,
"grade": "D",
"region": "NA"
},
{
"id": "d107f420-884a-53fb-beb8-b3d3c0985946",
"name": "Signal Software",
"icp_score": 35,
"grade": "D",
"region": "NA"
},
{
"id": "4b1b13ea-126a-54d9-97f4-f9a82bbe5fa8",
"name": "Driftwood Pay",
"icp_score": 30,
"grade": "D",
"region": "NA"
},
{
"id": "83f85184-7295-5924-90cf-58552ba5d7dd",
"name": "Garnet Health",
"icp_score": 24,
"grade": "D",
"region": "NA"
}
],
"traced_accounts": 6
}6 accounts traced through all 4 links
1. Domain missing
9 live accounts have no domain.
accounts where domain is null and merged_into_id is null2. Enrichment cannot run
9 of them have never had an enrichment run finish: the waterfall is keyed on the domain and marks the run failed without one.
… and no enrichment_runs row with status <> 'failed'9 accounts traced through all 3 links
1. Sources contradict each other
15 accounts carry a material provider conflict on employee count or industry.
field_provenance where conflict is not null and conflict->>'material' is true2. The derived value changes
For 9 of them the rejected answer lands in a different segment band or a different ICP industry tier — the disagreement is not cosmetic, it changes what the account *is*.
segment_for(kept) <> segment_for(rejected), or ICP tier(kept) <> tier(rejected)87 accounts traced through all 3 links
1. Region no rule names
294 non-customer accounts sit in a region no active routing rule mentions.
accounts where region not in ['EMEA', 'NA'] and segment is not null and is_customer = false2. Routed and unmatched
87 of them were routed in the last 90 days and matched nothing.
At least one system is failing badly enough to leak pipeline today.
Accounts stalled in the funnel right now
Ranked by revenue impact · 176 priority accounts across 7 fixes
Last successful sync 8h ago · 0.2% record failures (7d)
3. Never reaches A or B
9 of those are graded C, D or not at all — no firmographics, no fit points.
… and score_grade not in ('A','B')4. Never contacted
6 have never been sent a single email.
… and no activity of type email_sent, everAccounts invisible to targeting
6 accounts cannot be enriched, cannot score, and have never been contacted. They are not rejected targets; they were never candidates.
Fix
Resolve the domain first, from the contact emails already on the account
Before the enrichment waterfall, derive the domain from the most common non-free email domain among the account's contacts, then enrich; queue the rest for manual resolution.
Expected effect: 9 accounts become enrichable; each one that scores at the average of its enriched peers gains the difference in the metrics below.
How sure is thismechanism + correlation
The block itself is mechanical and readable in the code: no domain, no waterfall. The score and outreach gaps are consistent with it.
What it does not prove: Only 9 accounts are in the root set, far too few to treat the score difference as a rate. Nothing here shows these accounts would be good targets if enriched — only that the question has never been asked of them.
{
"key": "domain_gap_blocks_enrichment",
"title": "Missing domain → enrichment cannot run → no firmographics → account never targeted",
"root_cause": "Domain missing",
"consequence_label": "Accounts invisible to targeting",
"links": [
{
"step": 1,
"label": "Domain missing",
"count": 9,
"sentence": "9 live accounts have no domain.",
"query": "accounts where domain is null and merged_into_id is null",
"evidence": {}
},
{
"step": 2,
"label": "Enrichment cannot run",
"count": 9,
"sentence": "9 of them have never had an enrichment run finish: the waterfall is keyed on the domain and marks the run failed without one.",
"query": "… and no enrichment_runs row with status <> 'failed'",
"evidence": {
"code": "services/enrichment_service.py sets run.status = 'failed' when account.domain is empty",
"also_missing_another_core_field": 4
}
},
{
"step": 3,
"label": "Never reaches A or B",
"count": 9,
"sentence": "9 of those are graded C, D or not at all — no firmographics, no fit points.",
"query": "… and score_grade not in ('A','B')",
"evidence": {
"avg_icp_score_without_domain": 30.8,
"avg_icp_score_with_domain": 40.9
}
},
{
"step": 4,
"label": "Never contacted",
"count": 6,
"sentence": "6 have never been sent a single email.",
"query": "… and no activity of type email_sent, ever",
"evidence": {}
}
],
"consequence": {
"label": "Accounts invisible to targeting",
"accounts": 6,
"sentence": "6 accounts cannot be enriched, cannot score, and have never been contacted. They are not rejected targets; they were never candidates.",
"metrics": {
"avg_icp_score_without_domain": 30.8,
"avg_icp_score_with_domain": 40.9,
"accounts_never_emailed": 6
}
},
"fix": {
"title": "Resolve the domain first, from the contact emails already on the account",
"how": "Before the enrichment waterfall, derive the domain from the most common non-free email domain among the account's contacts, then enrich; queue the rest for manual resolution.",
"expected_effect": "9 accounts become enrichable; each one that scores at the average of its enriched peers gains the difference in the metrics below.",
"rec_key": "enrichment_waterfall"
},
"confidence": {
"level": "medium",
"basis": "mechanism + correlation",
"statement": "The block itself is mechanical and readable in the code: no domain, no waterfall. The score and outreach gaps are consistent with it.",
"limits": "Only 9 accounts are in the root set, far too few to treat the score difference as a rate. Nothing here shows these accounts would be good targets if enriched — only that the question has never been asked of them."
},
"accounts": [
{
"id": "6d2890ba-0876-503f-a31a-9533908b2980",
"name": "Cobalt Logic",
"icp_score": 40,
"grade": "D",
"region": "NA"
},
{
"id": "71561a30-ec4f-5d29-80a5-7d58491b2cdd",
"name": "Quarry Intelligence",
"icp_score": 39,
"grade": "D",
"region": "EMEA"
},
{
"id": "405d7d59-abe2-57c9-beb8-26aba55f289c",
"name": "Beacon Robotics Studio",
"icp_score": 27,
"grade": "D",
"region": "EMEA"
},
{
"id": "b4dfc820-0dce-518f-b6cb-e72090b6fcd7",
"name": "Ion IO",
"icp_score": 24,
"grade": "D",
"region": "NA"
},
{
"id": "f080cef5-d4ef-5f45-b24c-0cf4b86cf761",
"name": "Atlas Metrics One, Inc.",
"icp_score": 15,
"grade": "D",
"region": null
},
{
"id": "b5d2b8e2-4c80-5587-a453-1098f0e0ec0f",
"name": "Quill Systems, Inc.",
"icp_score": 0,
"grade": "D",
"region": null
}
],
"traced_accounts": 6
}3. A live decision rests on it
9 of those already have an owner chosen by a segment-conditioned rule, or a fit score taking full credit for a core industry the other source disputes.
routing_decisions.rule_id in (rules testing account.segment), or score_components.industry at max pointsOwners and grades resting on a coin flip
9 accounts are routed or scored on a value that a second confident source contradicts. Nothing in the record says which source is right.
Fix
Send material conflicts to a human decision with a manual lock
Re-run enrichment on the conflicting field; if the sources still disagree, queue the account for a one-click decision that writes a manual lock so the next run cannot flip it back.
Expected effect: 9 routing and scoring decisions stop depending on which provider answered last; the lock makes the choice auditable.
How sure is thismechanism (exposure, not error)
The band and tier arithmetic is deterministic, so 'the derived value would change' is a fact about the two values, not an estimate.
What it does not prove: This measures exposure, not mistakes. It does not claim the current owner or grade is wrong — only that the data does not settle it, and that a re-run could silently change it.
{
"key": "provider_conflict_decides_owner",
"title": "Providers disagree → the derived segment or industry flips → the owner and the grade are a coin flip",
"root_cause": "Sources contradict each other",
"consequence_label": "Owners and grades resting on a coin flip",
"links": [
{
"step": 1,
"label": "Sources contradict each other",
"count": 15,
"sentence": "15 accounts carry a material provider conflict on employee count or industry.",
"query": "field_provenance where conflict is not null and conflict->>'material' is true",
"evidence": {}
},
{
"step": 2,
"label": "The derived value changes",
"count": 9,
"sentence": "For 9 of them the rejected answer lands in a different segment band or a different ICP industry tier — the disagreement is not cosmetic, it changes what the account *is*.",
"query": "segment_for(kept) <> segment_for(rejected), or ICP tier(kept) <> tier(rejected)",
"evidence": {
"segment_band_flips": 6,
"industry_tier_flips": 3,
"size_conflicts": 10,
"industry_conflicts": 5
}
},
{
"step": 3,
"label": "A live decision rests on it",
"count": 9,
"sentence": "9 of those already have an owner chosen by a segment-conditioned rule, or a fit score taking full credit for a core industry the other source disputes.",
"query": "routing_decisions.rule_id in (rules testing account.segment), or score_components.industry at max points",
"evidence": {}
}
],
"consequence": {
"label": "Owners and grades resting on a coin flip",
"accounts": 9,
"sentence": "9 accounts are routed or scored on a value that a second confident source contradicts. Nothing in the record says which source is right.",
"metrics": {
"segment_band_flips": 6,
"industry_tier_flips": 3,
"decisions_resting_on_disputed_values": 9
}
},
"fix": {
"title": "Send material conflicts to a human decision with a manual lock",
"how": "Re-run enrichment on the conflicting field; if the sources still disagree, queue the account for a one-click decision that writes a manual lock so the next run cannot flip it back.",
"expected_effect": "9 routing and scoring decisions stop depending on which provider answered last; the lock makes the choice auditable.",
"rec_key": null
},
"confidence": {
"level": "high",
"basis": "mechanism (exposure, not error)",
"statement": "The band and tier arithmetic is deterministic, so 'the derived value would change' is a fact about the two values, not an estimate.",
"limits": "This measures exposure, not mistakes. It does not claim the current owner or grade is wrong — only that the data does not settle it, and that a re-run could silently change it."
},
"accounts": [
{
"id": "4896372a-9e22-5cf9-876c-2d37d40c3637",
"name": "Brightline Networks",
"icp_score": 65,
"grade": "B",
"region": "EMEA"
},
{
"id": "b8b56720-68ba-5492-853d-6fe77dacb322",
"name": "Brightline Labs",
"icp_score": 62,
"grade": "B",
"region": "NA"
},
{
"id": "cb6631e6-d902-53ec-8725-4129da5064ee",
"name": "Willow Cloud",
"icp_score": 61,
"grade": "B",
"region": "NA"
},
{
"id": "d25c5914-9109-5451-afb2-0bfec2e3e33e",
"name": "Sable Works",
"icp_score": 60,
"grade": "B",
"region": "NA"
},
{
"id": "19adadd1-6237-5d4c-a818-8d56e5c5da42",
"name": "Nimbus Logic",
"icp_score": 59,
"grade": "B",
"region": "NA"
},
{
"id": "a5a4843b-d355-5ff7-a205-5d081743c171",
"name": "Sable Dynamics",
"icp_score": 59,
"grade": "B",
"region": "EMEA"
}
],
"traced_accounts": 9
}… and exists (routing_decisions where outcome = 'unmatched' and decided_at >= now() - 90 days)3. No owner, no follow-up
87 still have no owner — triage is where they stopped, not a stage they passed through.
… and owner_id is nullAccounts stranded by a gap in the rule set
87 accounts have no territory owner because of a gap in the rule set rather than anything about the accounts; 6 of them are graded A or B, and the oldest has been waiting 89 days.
Fix
Give every region a destination, including a catch-all
Add a rule per uncovered region, and one fallback rule with no region condition so a new region is never silently unroutable.
Expected effect: 87 accounts get an owner immediately, 6 of them A/B; a fallback rule stops the next new region repeating it.
How sure is thismechanism + correlation
The gap is readable in the rule set: no active rule names these regions, and the unmatched decisions are recorded against exactly those accounts.
What it does not prove: The low ownership rate in these regions is consistent with the gap but not proof of it — headcount is allocated by region too. The open-pipeline figure is what exists today, not a forecast of what these accounts would be worth if worked.
{
"key": "territory_gap_strands_accounts",
"title": "No rule covers the region → nothing matches → the account falls to the triage queue",
"root_cause": "Region no rule names",
"consequence_label": "Accounts stranded by a gap in the rule set",
"links": [
{
"step": 1,
"label": "Region no rule names",
"count": 294,
"sentence": "294 non-customer accounts sit in a region no active routing rule mentions.",
"query": "accounts where region not in ['EMEA', 'NA'] and segment is not null and is_customer = false",
"evidence": {
"covered_regions": [
"EMEA",
"NA"
]
}
},
{
"step": 2,
"label": "Routed and unmatched",
"count": 87,
"sentence": "87 of them were routed in the last 90 days and matched nothing.",
"query": "… and exists (routing_decisions where outcome = 'unmatched' and decided_at >= now() - 90 days)",
"evidence": {
"unmatched_by_region": {
"APAC": 61,
"LATAM": 26
}
}
},
{
"step": 3,
"label": "No owner, no follow-up",
"count": 87,
"sentence": "87 still have no owner — triage is where they stopped, not a stage they passed through.",
"query": "… and owner_id is null",
"evidence": {
"oldest_unmatched_days": 89,
"graded_a_or_b": 6
}
}
],
"consequence": {
"label": "Accounts stranded by a gap in the rule set",
"accounts": 87,
"sentence": "87 accounts have no territory owner because of a gap in the rule set rather than anything about the accounts; 6 of them are graded A or B, and the oldest has been waiting 89 days.",
"metrics": {
"high_fit_accounts_stranded": 6,
"icp_score_at_risk": 3602,
"open_pipeline_usd": 448000,
"oldest_unmatched_days": 89,
"unowned_share_by_region": {
"unknown": 1,
"EMEA": 0.359,
"LATAM": 0.301,
"NA": 0.344,
"APAC": 0.369
}
}
},
"fix": {
"title": "Give every region a destination, including a catch-all",
"how": "Add a rule per uncovered region, and one fallback rule with no region condition so a new region is never silently unroutable.",
"expected_effect": "87 accounts get an owner immediately, 6 of them A/B; a fallback rule stops the next new region repeating it.",
"rec_key": "territory_gap"
},
"confidence": {
"level": "high",
"basis": "mechanism + correlation",
"statement": "The gap is readable in the rule set: no active rule names these regions, and the unmatched decisions are recorded against exactly those accounts.",
"limits": "The low ownership rate in these regions is consistent with the gap but not proof of it — headcount is allocated by region too. The open-pipeline figure is what exists today, not a forecast of what these accounts would be worth if worked."
},
"accounts": [
{
"id": "62d79e9f-1617-5948-8649-4a619ac339d8",
"name": "Driftwood Intelligence",
"icp_score": 64,
"grade": "B",
"region": "APAC"
},
{
"id": "389e1763-7b74-5e82-a04c-f2cf1b7b0d43",
"name": "Osprey Analytics",
"icp_score": 64,
"grade": "B",
"region": "APAC"
},
{
"id": "9534f144-ebb6-5bc7-8a76-e2f0c89b4b08",
"name": "Glacier Pay",
"icp_score": 62,
"grade": "B",
"region": "APAC"
},
{
"id": "30fbf9d9-9ec3-561e-814f-f136392dbef5",
"name": "Osprey Labs",
"icp_score": 61,
"grade": "B",
"region": "LATAM"
},
{
"id": "fb83ffed-ede4-5086-889d-2fc7c8f5df18",
"name": "Wren Metrics",
"icp_score": 60,
"grade": "B",
"region": "LATAM"
},
{
"id": "e4540c8f-4dc0-589a-ad5a-fa681540fa4c",
"name": "Sparrow Works",
"icp_score": 59,
"grade": "B",
"region": "APAC"
}
],
"traced_accounts": 87
}{
"runs_7d": 7,
"failed_runs_7d": 0,
"partial_runs_7d": 1,
"records_changed_7d": 1286,
"records_failed_7d": 2,
"retries_7d": 35,
"bad_external_ids": 6,
"duplicate_accounts": 6
}85% of accounts fully enriched · 15% incomplete · 310 stale
| Provider | Hit rate | Error rate | Cost credits (30d) |
|---|---|---|---|
| apollo | n/a | n/a | 0 |
| demo_firmographics | 79.8% | 5.2% | 151 |
| demo_hiring | 66.2% | 2.0% | 113.3 |
| demo_webscan | 63.5% | 0.7% | 75.5 |
{
"accounts": 2006,
"fully_enriched": 1714,
"field_coverage": {
"domain": 0.9955,
"industry": 0.9472,
"employee_count": 0.9816,
"region": 0.999,
"ai_team_size": 0.9187
},
"stale_accounts": 310,
"providers": [
{
"provider": "apollo",
"hit_rate": null,
"error_rate": null,
"cost_credits_30d": 0
},
{
"provider": "demo_firmographics",
"hit_rate": 0.7978,
"error_rate": 0.0523,
"cost_credits_30d": 151
},
{
"provider": "demo_hiring",
"hit_rate": 0.6623,
"error_rate": 0.0199,
"cost_credits_30d": 113.25
},
{
"provider": "demo_webscan",
"hit_rate": 0.6351,
"error_rate": 0.0068,
"cost_credits_30d": 75.5
}
]
}0 A/B accounts unowned · 95 unmatched decisions (90d) · 13 rule conflicts resolved
{
"unmatched_by_region_90d": {
"APAC": 61,
"EMEA": 2,
"LATAM": 26,
"NA": 6
},
"conflicts_90d": 13,
"unowned_ab_accounts": 0,
"over_capacity_owners": [
"Morgan Reyes"
],
"inactive_owners_with_accounts": {
"Chris Walker": 13
},
"median_signal_to_owner_hours": 75
}18 duplicate contacts · 35 accounts missing employee count · 504 open issues
{
"open_by_rule": {
"bad_external_id": 6,
"duplicate_account": 6,
"duplicate_contact": 18,
"invalid_email": 81,
"invalid_pipeline_transition": 2,
"lifecycle_conflict": 8,
"missing_domain": 9,
"missing_employee_count": 35,
"orphan_contact": 14,
"provider_conflict": 15,
"stale_enrichment": 310
},
"open_by_severity": {
"medium": 155,
"high": 39,
"low": 310
}
}Last product event 25h ago · 6.1% webhook failures (7d) · 0 unmatched events
{
"webhooks_7d": 33,
"failure_rate_7d": 0.0606,
"duplicates_absorbed_7d": 5,
"engagement_events": 6335,
"unmatched_events": 0
}Incomplete for 18% of opportunities (180d)
{
"opportunities_180d": 77,
"unattributed_opportunities": 14,
"unattributed_pipeline": 1074000
}7.7% of executed runs failed (30d) · 19 in dead letter · 21 steps recovered by retry
{
"by_status_30d": {
"succeeded": 240,
"skipped": 427,
"failed": 7,
"dead_letter": 13
},
"dead_letter_total": 19,
"retried_steps_30d": 21
}2.1% bounce rate (30d) · 66 invalid emails not suppressed
Note: Deliverability also depends on SPF/DKIM/DMARC and domain warm-up, which GTMOS does not manage.
{
"sent_30d": 1310,
"bounced_30d": 27,
"invalid_unsuppressed": 66,
"note": "Deliverability also depends on SPF/DKIM/DMARC and domain warm-up, which GTMOS does not manage."
}