Article
Fuzzing the Assignment Matrix: Building a Lead Routing Rule Simulator with Claude Code
Most CRM lead assignment systems are deceptively simple: they are waterfalls. Whether you are using Salesforce Lead Assignment Rules or HubSpot’s branching workflow logic, the engine evaluates criteria from top to bottom and stops the moment it finds a match.
In a small organization, this is manageable. But as GTM teams scale, the routing matrix becomes a minefield. You add a new territory, an industry segment, and a partner-led carve-out. Suddenly, you have priority inversions where a broad catch-all rule swallows a high-priority lead before it reaches a more specific rule further down the list. Or worse, you create a dead zone: a combination of industry and region that matches no rules at all, leaving high-intent leads to rot in a default queue.
Testing these changes in production is a high-stakes experiment. Instead, we can apply a bit of GTM engineering rigor. By translating our routing logic into a local configuration and using Claude Code to build a deterministic simulator, we can "fuzz" the matrix—bombarding it with hundreds of synthetic edge cases to find the gaps before a single lead is misrouted.
The Mechanics of Silent Failure
Native CRM builders are designed for configuration, not validation. Salesforce evaluations are strictly linear; if entry #4 matches, entries #5 through #50 are never evaluated. This creates a specific failure mode: the priority inversion. If Rule #4 is Country = 'US' and Rule #12 is Country = 'US' AND Employees > 5000, the Enterprise lead will always be captured by the general US rule. The system doesn't flag this as an error because, technically, the logic is valid. It’s just not what you intended.
HubSpot faces a different version of this in workflows. If a routing branch is configured to rotate leads among a specific team, but that team is currently empty or users are unavailable, the record can simply stall or default to an unassigned state. These are "silent failures" because the automation technically completed its check—it just didn't result in a successful assignment.
Native UIs make it nearly impossible to visualize these overlaps. You are looking at a list of rules, but you aren't looking at the volume of leads that will hit them or the edge cases that will slip between them.
Step 1: Translating UI Logic to Configuration
To simulate routing, we first need to extract the logic from the CRM UI into a machine-readable format. I prefer YAML for this because it’s easy for humans to read and for scripts to parse.
Here is a simplified example of what a routing configuration looks like:
rules:
- id: "001"
name: "Strategic Enterprise - US"
criteria:
country: ["US", "USA", "United States"]
employees_min: 5001
target: "Enterprise_US_Queue"
- id: "002"
name: "EMEA Central"
criteria:
country: ["DE", "AT", "CH"]
target: "EMEA_Central_Team"
- id: "catch-all"
name: "Default Inbound"
criteria: {}
target: "Default_Queue"
By defining our rules this way, we create a single source of truth that we can test against without clicking through dozens of UI screens. It also allows us to version control our routing logic in Git, providing a history of who changed what and why.
Step 2: Building the Simulator with Claude Code
Using Claude Code, we can quickly build a Python-based evaluator that mimics the CRM's top-to-bottom logic. The goal isn't to build a new routing engine, but to build a mirror of our existing one.
I initialized a new directory and let Claude Code handle the scaffolding. My prompt was specific about the logic:
"Write a Python script
simulator.pythat loadsrouting_rules.yamland atest_leads.jsonfile. The script should iterate through leads and find the first matching rule based on the criteria. Handle list matches (e.g., country in [US, USA]) and numeric boundaries (e.g., employees_min). Output a CSV of results and flag any lead that hits the 'catch-all' or remains 'Unassigned'."
Claude Code produced a robust evaluator. Here is the core logic it generated for the matching engine:
def evaluate_lead(lead, rules):
for rule in rules:
criteria = rule.get('criteria', {})
if not criteria: # Catch-all rule
return rule['id'], rule['name'], rule['target']
is_match = True
for key, required_val in criteria.items():
lead_val = lead.get(key)
if isinstance(required_val, list):
if lead_val not in required_val:
is_match = False
break
elif key.endswith('_min'):
base_key = key.replace('_min', '')
lead_val = lead.get(base_key)
if lead_val is None or int(lead_val) < required_val:
is_match = False
break
elif lead_val != required_val:
is_match = False
break
if is_match:
return rule['id'], rule['name'], rule['target']
return "NONE", "No Match Found", "Unassigned"
Step 3: Fuzzing for Edge Cases
The real value comes from "fuzzing": generating synthetic lead data designed to break your logic. I used Claude to generate a test_leads.json array of 100 leads that target common GTM data gaps:
- Null Values: Leads where
countryoremployeesare missing entirely. - Boundary Values: A lead with exactly 5000 employees when the rule starts at 5001.
- Data Conflicts: A lead where the country is
DE(Germany) but the email domain is.uk. - Priority Clashes: A lead that fits both a "Strategic Enterprise" and a "US Mid-Market" criteria to see which one it hits first.
When you run the simulator against this fuzzed dataset, the output reveals the gaps. In my test run, I discovered that 12% of leads were hitting the catch-all queue simply because they had USA instead of United States—a variation I hadn't included in the original rule list.
The Trade-offs of Local Simulation
This approach introduces a maintenance burden. You must keep your YAML configuration in sync with your CRM. If you change a rule in Salesforce but forget to update the YAML, your simulator is lying to you. For small teams with three or four simple rules, this is likely overkill.
Furthermore, static simulators struggle with dynamic state. If your routing depends on real-time rep availability, vacation calendars, or round-robin counters, a local script won't capture that perfectly without pulling data from external APIs (like the Chili Piper or Calendly APIs).
There are also dedicated routing vendors like LeanData or Chili Piper that provide visual logic graphs and native validation. These tools are excellent but come with significant price tags and contract lock-in. If you are already paying for them, use their native tools. But if you are managing complex native rules on a budget, this GTM engineering approach gives you the same level of confidence without the $20k/year platform fee.
Moving Toward GTM Engineering
When lead routing fails, the cost is lost revenue. A lead that sits in a dead zone for 24 hours is a lead that's likely already talking to a competitor.
By treating your routing matrix as a testable configuration, you move from reactive troubleshooting to proactive engineering. You can run your simulation before every major territory realignment, ensuring that when the leads start flowing, the waterfall lands exactly where it’s intended.
— C.B.