# How do I route inbound leads to the right rep with AI?

> Let AI classify each inbound lead and let fixed rules pick the rep. Compare HubSpot, Salesforce, LeanData, Chili Piper, Default and a DIY webhook build.

- Published: 2026-10-28
- URL: https://teton.so/guides/route-inbound-leads-to-the-right-rep-with-ai

Have the AI read the lead and return one value from a fixed list, such as segment, product line or fit. Then let ordinary rules pick the person: existing account owner first, then territory or team, then round-robin within that team, with a fallback owner when anything is unclear. Do not let the model choose a rep by name. You can build this inside HubSpot workflows with an AI action, in Salesforce with assignment rules and Agentforce, with a routing tool such as LeanData, Chili Piper or Default, or with a small webhook job that calls Claude and writes the owner back to your CRM.

Below is the routing order to use, how each option implements it, and how to test it before it touches real leads.

## Let the AI classify and the rules assign

Most serious write-ups use the same split. The model handles the part rules are bad at: reading a free-text "tell us about your project" field, guessing company size from a domain, or spotting a partner inquiry dressed up as a demo request. The rules handle the parts that must be predictable: who owns the account, who is on which team, whose turn it is, and who is out this week.

This split matters for three reasons:

- **Fairness.** Reps accept round-robin because it is visible and even. A model choosing a rep is neither.
- **Debugging.** When a lead lands in the wrong place, you can see whether the category was wrong or the rule was wrong.
- **Failure handling.** If the model times out or returns something unexpected, the lead still reaches a person.

## The routing order

1. **Check for an existing owner.** If the contact or their company already has an owner, assign to that owner. This prevents two reps from working the same account.
2. **Filter out non-leads.** Spam, job seekers, vendors pitching you and students go to a queue or nowhere.
3. **Classify.** The AI returns one allowed category and a short reason.
4. **Map category to team.** A plain lookup: enterprise goes to the enterprise team, partners to the partner manager.
5. **Pick the rep within the team.** Round-robin, least recently assigned, or capacity-capped.
6. **Fallback.** Low confidence, an unknown category or an error goes to one named owner for manual review.
7. **Follow up on speed.** Create a task or alert so the assignment turns into a response. Routing vendors such as [Default](https://www.default.com/post/inbound-lead-qualification) argue that even a five-minute delay hurts booking rates; that is a vendor claim, but slow follow-up is the common failure.

## Option 1: HubSpot workflows

HubSpot's own building blocks cover most of this. In **Automation > Workflows**, create a contact-based workflow triggered by a form submission. Use **Set property value** on Contact owner for one-to-one routing, or **Rotate record to owner** to spread leads across a team ([walkthrough](https://www.simular.ai/workflow/how-to-build-advanced-hubspot-lead-routing-systems)).

Two details to get right:

- Add "Contact owner is unknown" to the enrollment criteria so the workflow never overwrites an existing owner.
- Account matching is not built in. You have to branch in the workflow yourself: if an owner exists, keep it; otherwise rotate ([Hublead](https://www.hublead.io/blog/lead-form-routing-hubspot)).

For the AI step, a Data Agent action or the Run agent action can return an answer you branch on before **Rotate record to owner**. This requires Professional or Enterprise, and owners must be active users with paid Sales Hub or Service Hub seats ([InsideA](https://insidea.com/blog/hubspot/kb/ai-agents-and-intelligent-crm-automation-in-hubspot)). No Bounds Digital's [worked example](https://noboundsdigital.com/blog/stop-guessing-where-leads-belong-ai-powered-routing-in-hubspot) writes the agent's answer to a custom "AI Lead Routing Category" property and adds a fallback branch for answers that don't match an approved category, so those leads go to a general owner instead of being lost. Copy that fallback.

**Fits when:** you are on HubSpot Pro or Enterprise and your rules fit in workflow branches.

## Option 2: Salesforce assignment rules and Agentforce

Lead assignment rules are the usual starting point in Salesforce. Their weakness is maintenance: someone writes them, someone keeps them current, and nothing tells you when a rule sends a lead to the wrong place ([LeanData](https://www.leandata.com/blog/salesforce-lead-routing-automation/), a routing vendor). Partners describe building Agentforce SDR-style agents that [qualify the lead and call Salesforce flows for assignment](https://www.pedowitzgroup.com/agentforce-help-lead-qualification-routing), which keeps the same split: the agent judges, a flow assigns. Agent Engagement Rules decide [which records the agent works on](https://www.salesforceben.com/nurture-leads-24-7-with-agentforce-for-sales/).

Be skeptical of pages that say an agent can route by "historical rep performance" without showing configuration.

**Fits when:** you are a Salesforce shop with an admin who already maintains flows.

## Option 3: Dedicated routing tools

**LeanData** has an AI Inference Node that runs a prompt your team writes against CRM fields and returns structured output for routing. It supports OpenAI and Gemini with your own key, has a preview tool for checking the AI's reasoning before activation, and offers plain-language summaries of why a match happened ([LeanData](https://www.leandata.com/blog/ai-gtm-lead-routing-use-cases/)).

**Chili Piper** routes and books meetings at form submit, and a competitor's review describes an assistant that builds routing rules from natural-language instructions ([Knock AI](https://www.knock-ai.com/blog/chili-piper-review)).

**Default** offers round-robin modes such as "Take turns equally" and "Least recent assignment." Its HubSpot docs note that each team member needs an owner mapping or the owner write fails for that person ([Default docs](https://docs.os.default.com/lead-routing/hubspot)).

Most of what is published about these tools comes from the vendors or their competitors, so test against your own leads.

**Fits when:** routing is complex (territories, account matching, booking a meeting on the spot) and you would rather pay for a tool than maintain logic.

## Option 4: Build it yourself with a webhook and Claude

If your rules don't fit a vendor's model, a small script does the whole order above. Zapier can glue it together, though polling triggers [add delay](https://getbreakout.ai/blog/inbound-lead-routing-zapier-sales-reps); a webhook from the form is faster.

This script checks the account owner in HubSpot, asks Claude for one category using a forced tool call so the output is always valid JSON from an allowed list, rotates within the team using a Postgres counter, and falls back on any doubt. Install with `pip install anthropic requests "psycopg[binary]"` and set `ANTHROPIC_API_KEY`, `HUBSPOT_TOKEN` and `DATABASE_URL`.

```sql
create table if not exists rr_state (
  segment text primary key,
  next_index int not null default 0
);
```

```python
import os, json, requests, anthropic, psycopg

API = "https://api.hubapi.com"
H = {"Authorization": f"Bearer {os.environ['HUBSPOT_TOKEN']}"}
SEGMENTS = ["enterprise", "mid_market", "smb", "partner", "not_a_lead"]
TEAMS = {  # HubSpot owner IDs; replace with your own
    "enterprise": ["111", "112"],
    "mid_market": ["211", "212", "213"],
    "smb": ["311", "312"],
    "partner": ["411"],
}
FALLBACK_OWNER = "999"
FREEMAIL = {"gmail.com", "outlook.com", "yahoo.com", "hotmail.com"}
client = anthropic.Anthropic()

def account_owner(domain):
    if domain in FREEMAIL:
        return None
    r = requests.post(f"{API}/crm/v3/objects/companies/search", headers=H, timeout=30, json={
        "filterGroups": [{"filters": [{"propertyName": "domain", "operator": "EQ", "value": domain}]}],
        "properties": ["hubspot_owner_id"], "limit": 1})
    r.raise_for_status()
    results = r.json()["results"]
    return results[0]["properties"].get("hubspot_owner_id") if results else None

def classify(lead):
    msg = client.messages.create(
        model=os.environ.get("CLAUDE_MODEL", "claude-sonnet-4-5"),
        max_tokens=400,
        system=open("routing_rules.md").read(),
        tools=[{
            "name": "route_lead",
            "description": "Record the routing category for an inbound lead.",
            "input_schema": {
                "type": "object",
                "properties": {
                    "segment": {"type": "string", "enum": SEGMENTS},
                    "confidence": {"type": "string", "enum": ["high", "low"]},
                    "reason": {"type": "string"},
                },
                "required": ["segment", "confidence", "reason"],
            },
        }],
        tool_choice={"type": "tool", "name": "route_lead"},
        messages=[{"role": "user", "content": json.dumps(lead)}],
    )
    return next(b for b in msg.content if b.type == "tool_use").input

def next_rep(conn, segment):
    reps = TEAMS[segment]
    (i,) = conn.execute(
        """insert into rr_state (segment, next_index) values (%s, 1)
           on conflict (segment) do update set next_index = rr_state.next_index + 1
           returning next_index - 1""", (segment,)).fetchone()
    conn.commit()
    return reps[i % len(reps)]

def route(conn, lead):
    owner = account_owner(lead["email"].split("@")[-1].lower())
    if owner:
        return owner, "existing account owner"
    try:
        c = classify(lead)
    except Exception as e:
        return FALLBACK_OWNER, f"classifier failed: {e}"
    if c["segment"] == "not_a_lead":
        return None, c["reason"]
    if c["confidence"] == "low" or c["segment"] not in TEAMS:
        return FALLBACK_OWNER, "needs review: " + c["reason"]
    return next_rep(conn, c["segment"]), c["reason"]

def assign(contact_id, owner_id):
    r = requests.patch(f"{API}/crm/v3/objects/contacts/{contact_id}", headers=H, timeout=30,
                       json={"properties": {"hubspot_owner_id": owner_id}})
    r.raise_for_status()

if __name__ == "__main__":
    lead = {"contact_id": "12345", "email": "jane@example.com",
            "company": "Example Corp", "message": "We need this for 400 sales reps."}
    with psycopg.connect(os.environ["DATABASE_URL"]) as conn:
        owner, reason = route(conn, lead)
        if owner:
            assign(lead["contact_id"], owner)
        print(owner, reason)
```

`routing_rules.md` holds your definitions in plain language: what counts as enterprise, which signals mean partner, and when to answer `low`. Keep it short and concrete, with a few example leads per category.

**Fits when:** your rules are specific to your business, or you are on a CRM tier without AI workflow actions. The cost is that you host it, store secrets and keep it running.

## Running the DIY version without hosting it

[Teton](https://teton.so) runs Claude Code workflows headless in the cloud, triggered by a webhook, a schedule or by hand. You do the routing job once with Claude on a few real leads, type `/teton`, and Claude writes it up as one workflow file with the trigger, steps and credentials it needs. One deploy makes it live. A few parts map directly onto lead routing:

- Your ICP and qualified-lead definitions can live in shared context files. Workflows read them on every run, editing one changes the next run without a deploy, and each run records which revision it read, so you can tell whether a misroute came from a rule change.
- The steps run the same way each time; Claude is called only for the classification.
- Each workspace has its own Postgres database for the round-robin counter and a log of every routing decision.
- When a workflow writes to your CRM, each run lists exactly which records it changed.
- Credentials are entered once in the dashboard and only sent to the API hosts the workflow declares.

It is not a meeting-booking tool. If you need a calendar shown at form submit, Chili Piper or Default fits better.

## Test against past leads before going live

No vendor page shows this, so do it yourself. Export a few hundred recent leads where you know the right team. Run the classifier over them without writing anything, and compare its category with the known answer. Read every disagreement: some will be model errors, and many will show that your definitions were vague. Fix the rules file, rerun, and only then switch on writes. After launch, review the fallback queue weekly; if it grows, your categories need work.

## How to choose

| | Where the AI sits | Account-owner check | Rotation | Best when |
| --- | --- | --- | --- | --- |
| HubSpot workflows | Data Agent or Run agent action | Build it with branches | Rotate record to owner | HubSpot Pro or Enterprise, simple rules |
| Salesforce + Agentforce | Agent qualifies, flow assigns | Assignment rules or flows | Assignment rules or flows | Salesforce shop with an admin |
| LeanData | AI Inference Node, your own key | Yes | Yes | Complex territories and matching |
| Chili Piper / Default | Vendor AI features | Yes | Yes | Booking a meeting at form submit |
| DIY script | Your prompt, forced structured output | Your code | Postgres counter | Custom rules, any CRM tier |
| Teton | Claude step inside a fixed workflow | Your workflow | Workspace Postgres | Custom rules without hosting |

## FAQ

### Should the AI pick the rep directly?

No. Have it pick a category from a fixed list and let rules pick the rep. It keeps assignment fair, makes misroutes traceable, and gives you a clean fallback when the model fails.

### What happens if the AI step fails or times out?

Route to a named fallback owner and record why. Never leave the lead unassigned. HubSpot builds need a fallback branch; in code, wrap the model call and catch every error.

### Can I route by rep performance?

You can, but no source we found shows a tested setup, and it invites gaming and complaints about fairness. Start with fit and territory, then add capacity caps before trying performance weighting.

### How do I handle reps who are out of office?

Keep the team list as data, not in the prompt. Remove someone from the rotation while they are away, and reassign any lead not touched within your response deadline.

---

- [All guides](https://teton.so/guides)
- [Teton](https://teton.so): the cloud for Claude Code. Build a workflow with Claude once and Teton runs it on a schedule or a webhook.
