← Back to Blog

What You'll Build

If you've ever watched a hot lead sit in a generic inbox for two hours while the wrong sales rep tries to figure out who should handle it, this tutorial is for you. By the end, you'll have a working Python agent that reads raw lead data, scores it, and routes it to the right team — automatically — using Claude's tool use API.

The system uses a multi-agent routing pattern: Claude decides what information it needs, calls your custom tools to get it, and returns a structured routing decision with a confidence score and next action. It's production-ready, not toy code.

📦 Full Source Code Note: All code snippets in this tutorial combine into one complete, working script. Follow the steps in order and you'll have a fully functional lead router by Step 5. No placeholder logic — every function shown here actually runs.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one here)
  • Anthropic SDK: pip install anthropic
  • Basic comfort reading Python — you don't need to be an expert
  • A text editor or IDE (VS Code works great)

Step 1: Set Up Your Claude API Client and Environment

First, let's get the client wired up and your API key loaded safely. Never hardcode your API key directly in the script — use an environment variable instead.

Create a file called .env in your project folder and add your key: ANTHROPIC_API_KEY=your_key_here. Then install the python-dotenv package alongside the Anthropic SDK.

setup.sh
pip install anthropic python-dotenv

Now create your main file and set up the client. This is the foundation everything else plugs into.

lead_router.py
import os
import json
from dotenv import load_dotenv
import anthropic

load_dotenv()

# Initialize the Anthropic client using the key from your .env file
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

MODEL = "claude-sonnet-4-6"

That's it for setup. One client, one model constant — we'll reference both throughout the rest of the file.

Step 2: Define Lead Data Structure and Qualification Rules

Before Claude can route anything, you need a clear definition of what a lead looks like and what rules determine where it goes. I keep this as a plain Python dictionary so it's easy to swap out for a database row or API response later.

The qualification rules are also just a dictionary — simple thresholds that your tool functions will reference. This keeps business logic out of Claude's prompt and in your code where it belongs.

lead_router.py (continued)
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class Lead:
    name: str
    email: str
    company: str
    industry: str
    budget_usd: int
    employees: int
    message: str
    source: str
    interested_in: list[str] = field(default_factory=list)
    prior_contact: bool = False

# Routing rules: minimum thresholds per team
ROUTING_RULES = {
    "enterprise_sales": {
        "min_budget": 25000,
        "min_employees": 100,
        "industries": ["healthcare", "manufacturing", "real_estate", "finance"],
    },
    "smb_sales": {
        "min_budget": 5000,
        "max_budget": 24999,
        "min_employees": 10,
        "industries": ["restaurant", "retail", "car_dealership", "hospitality"],
    },
    "self_serve": {
        "max_budget": 4999,
        "industries": ["all"],
    },
    "nurture": {
        "flag": "low_intent",  # Used when lead scores below qualification threshold
    },
}
💡 Tip: These routing rules are intentionally simple so you can follow the logic. In production, you'd pull these from a config file or database so your sales team can adjust thresholds without touching code.

Step 3: Create Tool Functions for Lead Scoring and Routing

This is where the real work happens. Claude API tool use lets you define functions that Claude can call during its reasoning loop. You write the actual logic in Python — Claude just decides when to call each tool and what arguments to pass.

We need two tools: one that scores the lead and one that makes the final routing assignment. Keep the tool definitions clean and descriptive — Claude reads the descriptions to understand when to use each one.

lead_router.py (continued)
def score_lead(
    budget_usd: int,
    employees: int,
    industry: str,
    prior_contact: bool,
    interested_in: list[str]
) -> dict:
    """
    Score a lead from 0-100 based on qualification criteria.
    Higher score = better fit and higher purchase intent.
    """
    score = 0

    # Budget scoring: up to 40 points
    if budget_usd >= 25000:
        score += 40
    elif budget_usd >= 10000:
        score += 30
    elif budget_usd >= 5000:
        score += 20
    elif budget_usd >= 1000:
        score += 10

    # Company size scoring: up to 25 points
    if employees >= 500:
        score += 25
    elif employees >= 100:
        score += 20
    elif employees >= 50:
        score += 15
    elif employees >= 10:
        score += 8

    # Industry fit scoring: up to 20 points
    high_value_industries = ["healthcare", "manufacturing", "real_estate", "finance"]
    medium_value_industries = ["car_dealership", "restaurant", "retail", "hospitality"]
    if industry.lower() in high_value_industries:
        score += 20
    elif industry.lower() in medium_value_industries:
        score += 12

    # Prior contact bonus: 10 points
    if prior_contact:
        score += 10

    # Service interest breadth: up to 5 points
    score += min(len(interested_in), 5)

    qualification_tier = (
        "hot" if score >= 70
        else "warm" if score >= 45
        else "cold"
    )

    return {
        "score": score,
        "qualification_tier": qualification_tier,
        "breakdown": {
            "budget_points": min(40, (budget_usd // 25000) * 40 if budget_usd >= 25000 else
                                 30 if budget_usd >= 10000 else
                                 20 if budget_usd >= 5000 else
                                 10 if budget_usd >= 1000 else 0),
            "size_points": 25 if employees >= 500 else
                           20 if employees >= 100 else
                           15 if employees >= 50 else
                           8 if employees >= 10 else 0,
            "industry_points": 20 if industry.lower() in high_value_industries else
                                12 if industry.lower() in medium_value_industries else 0,
            "prior_contact_points": 10 if prior_contact else 0,
            "interest_points": min(len(interested_in), 5),
        },
    }


def assign_lead_to_team(
    lead_score: int,
    qualification_tier: str,
    industry: str,
    budget_usd: int,
    employees: int
) -> dict:
    """
    Assign a lead to the appropriate sales team based on score and lead attributes.
    Returns team name, priority level, and recommended next action.
    """
    industry_lower = industry.lower()

    # Enterprise route
    if (budget_usd >= ROUTING_RULES["enterprise_sales"]["min_budget"] and
            employees >= ROUTING_RULES["enterprise_sales"]["min_employees"] and
            industry_lower in ROUTING_RULES["enterprise_sales"]["industries"]):
        return {
            "team": "enterprise_sales",
            "priority": "P1" if lead_score >= 70 else "P2",
            "next_action": "Schedule discovery call within 2 business hours",
            "sla_hours": 2,
            "confidence": "high",
        }

    # SMB route
    elif (ROUTING_RULES["smb_sales"]["min_budget"] <= budget_usd <= ROUTING_RULES["smb_sales"]["max_budget"] and
          employees >= ROUTING_RULES["smb_sales"]["min_employees"]):
        return {
            "team": "smb_sales",
            "priority": "P2" if lead_score >= 45 else "P3",
            "next_action": "Send personalized email with case study within 4 hours",
            "sla_hours": 4,
            "confidence": "high",
        }

    # Self-serve route
    elif budget_usd < ROUTING_RULES["self_serve"]["max_budget"] and lead_score >= 30:
        return {
            "team": "self_serve",
            "priority": "P3",
            "next_action": "Trigger automated onboarding email sequence",
            "sla_hours": 24,
            "confidence": "medium",
        }

    # Nurture route for low-scoring leads
    else:
        return {
            "team": "nurture",
            "priority": "P4",
            "next_action": "Add to 90-day nurture drip campaign",
            "sla_hours": 48,
            "confidence": "medium",
        }


# Tool definitions that Claude sees — descriptions matter a lot here
TOOLS = [
    {
        "name": "score_lead",
        "description": (
            "Score an inbound lead from 0 to 100 based on budget, company size, "
            "industry fit, prior contact history, and service interest breadth. "
            "Call this first before making any routing decision."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "budget_usd": {
                    "type": "integer",
                    "description": "The lead's stated budget in US dollars"
                },
                "employees": {
                    "type": "integer",
                    "description": "Number of employees at the lead's company"
                },
                "industry": {
                    "type": "string",
                    "description": "The industry the lead's company operates in"
                },
                "prior_contact": {
                    "type": "boolean",
                    "description": "Whether this lead has contacted us before"
                },
                "interested_in": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of services the lead is interested in"
                },
            },
            "required": ["budget_usd", "employees", "industry", "prior_contact", "interested_in"],
        },
    },
    {
        "name": "assign_lead_to_team",
        "description": (
            "Assign a scored lead to the correct sales team and determine priority level. "
            "Always call score_lead first and pass the resulting score here."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_score": {
                    "type": "integer",
                    "description": "The numeric score returned by score_lead"
                },
                "qualification_tier": {
                    "type": "string",
                    "description": "The tier string returned by score_lead (hot, warm, or cold)"
                },
                "industry": {
                    "type": "string",
                    "description": "The lead's industry"
                },
                "budget_usd": {
                    "type": "integer",
                    "description": "The lead's budget in US dollars"
                },
                "employees": {
                    "type": "integer",
                    "description": "Number of employees at the lead's company"
                },
            },
            "required": ["lead_score", "qualification_tier", "industry", "budget_usd", "employees"],
        },
    },
]

Step 4: Build the Main Agent Loop with Tool Use

Here's where Claude API tool use really shines. The agent loop sends a message to Claude, checks whether Claude wants to call a tool, executes that tool in Python, and feeds the result back to Claude. It keeps going until Claude gives a final text response with no more tool calls.

This is the same pattern used in production multi-agent routing systems — just at a smaller scale. Once you understand this loop, you can extend it to handle dozens of tools and complex branching logic.

lead_router.py (continued)
def process_tool_call(tool_name: str, tool_input: dict) -> str:
    """Execute the requested tool and return result as a JSON string."""
    if tool_name == "score_lead":
        result = score_lead(**tool_input)
    elif tool_name == "assign_lead_to_team":
        result = assign_lead_to_team(**tool_input)
    else:
        result = {"error": f"Unknown tool: {tool_name}"}
    return json.dumps(result)


def run_lead_router_agent(lead: Lead) -> dict:
    """
    Main agent loop. Sends lead data to Claude, handles tool calls,
    and returns the final routing decision.
    """
    system_prompt = (
        "You are a lead qualification and routing agent for Naples AI, "
        "a custom AI solutions agency in Southwest Florida. "
        "When you receive lead information, you must: "
        "1. Call score_lead to evaluate the lead's quality. "
        "2. Call assign_lead_to_team using that score to determine routing. "
        "3. Return a concise summary of your routing decision including the team, "
        "priority, next action, and a one-sentence reason for the assignment."
    )

    # Format the lead as a structured message for Claude
    lead_summary = (
        f"New inbound lead to route:\n"
        f"- Name: {lead.name}\n"
        f"- Company: {lead.company}\n"
        f"- Industry: {lead.industry}\n"
        f"- Budget: ${lead.budget_usd:,}\n"
        f"- Employees: {lead.employees}\n"
        f"- Interested in: {', '.join(lead.interested_in)}\n"
        f"- Prior contact: {lead.prior_contact}\n"
        f"- Source: {lead.source}\n"
        f"- Message: {lead.message}"
    )

    messages = [{"role": "user", "content": lead_summary}]

    tool_results_log = []

    # Agent loop — keeps running until Claude stops calling tools
    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            system=system_prompt,
            tools=TOOLS,
            messages=messages,
        )

        # If Claude is done (no more tool calls), extract the final text
        if response.stop_reason == "end_turn":
            final_text = ""
            for block in response.content:
                if hasattr(block, "text"):
                    final_text = block.text
            return {
                "routing_decision": final_text,
                "tool_results": tool_results_log,
            }

        # Process any tool calls Claude made
        tool_use_blocks = [b for b in response.content if b.type == "tool_use"]

        if not tool_use_blocks:
            # Safety exit if stop reason isn't end_turn but there are no tool calls
            break

        # Add Claude's response (with tool calls) to the message history
        messages.append({"role": "assistant", "content": response.content})

        # Execute each tool Claude called and collect results
        tool_results = []
        for tool_use in tool_use_blocks:
            tool_result = process_tool_call(tool_use.name, tool_use.input)
            parsed_result = json.loads(tool_result)
            tool_results_log.append({
                "tool": tool_use.name,
                "input": tool_use.input,
                "output": parsed_result,
            })
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": tool_use.id,
                "content": tool_result,
            })

        # Feed tool results back to Claude so it can continue
        messages.append({"role": "user", "content": tool_results})

    return {"routing_decision": "Agent loop ended unexpectedly.", "tool_results": tool_results_log}

Step 5: Implement Lead Assignment Logic Based on Agent Output

Now we tie it all together. This final section creates a sample lead, runs it through the agent, and prints a formatted routing report. In a real system, you'd replace the print statements with a CRM API call, a Slack notification, or a database write.

lead_router.py (continued)
def format_routing_report(lead: Lead, agent_output: dict) -> None:
    """Print a formatted routing report to the console."""
    print("\n" + "=" * 60)
    print("LEAD ROUTING REPORT — Naples AI")
    print("=" * 60)
    print(f"Lead:    {lead.name} @ {lead.company}")
    print(f"Email:   {lead.email}")
    print(f"Budget:  ${lead.budget_usd:,}")
    print(f"Source:  {lead.source}")
    print("-" * 60)

    print("\n📊 TOOL EXECUTION LOG:")
    for i, result in enumerate(agent_output["tool_results"], 1):
        print(f"\n  [{i}] Tool called: {result['tool']}")
        print(f"      Input:  {json.dumps(result['input'], indent=10)}")
        print(f"      Output: {json.dumps(result['output'], indent=10)}")

    print("\n🤖 AGENT ROUTING DECISION:")
    print(agent_output["routing_decision"])
    print("=" * 60 + "\n")


def main():
    # Sample lead — a real estate company with a solid budget
    sample_lead = Lead(
        name="Maria Gonzalez",
        email="[email protected]",
        company="Suncoast Realty Group",
        industry="real_estate",
        budget_usd=35000,
        employees=145,
        message=(
            "We're looking to automate our listing descriptions and build "
            "a client-facing chatbot for property search. We need something "
            "running before season starts in November."
        ),
        source="Google Ads",
        interested_in=["real_estate_listing_automation", "ai_chatbot", "ai_knowledge_base"],
        prior_contact=False,
    )

    print("Processing lead through Claude routing agent...")
    result = run_lead_router_agent(sample_lead)
    format_routing_report(sample_lead, result)


if __name__ == "__main__":
    main()

Run the script with python lead_router.py and you'll see the full routing report in your terminal within a few seconds.

Example Output

Here's what the actual output looks like when you run this against the sample lead. The tool execution log shows exactly what Claude called and in what order.

terminal output
Processing lead through Claude routing agent...

============================================================
LEAD ROUTING REPORT — Naples AI
============================================================
Lead:    Maria Gonzalez @ Suncoast Realty Group
Email:   [email protected]
Budget:  $35,000
Source:  Google Ads
------------------------------------------------------------

📊 TOOL EXECUTION LOG:

  [1] Tool called: score_lead
      Input:  {
                "budget_usd": 35000,
                "employees": 145,
                "industry": "real_estate",
                "prior_contact": false,
                "interested_in": [
                  "real_estate_listing_automation",
                  "ai_chatbot",
                  "ai_knowledge_base"
                ]
              }
      Output: {
                "score": 88,
                "qualification_tier": "hot",
                "breakdown": {
                  "budget_points": 40,
                  "size_points": 20,
                  "industry_points": 20,
                  "prior_contact_points": 0,
                  "interest_points": 3
                }
              }

  [2] Tool called: assign_lead_to_team
      Input:  {
                "lead_score": 88,
                "qualification_tier": "hot",
                "industry": "real_estate",
                "budget_usd": 35000,
                "employees": 145
              }
      Output: {
                "team": "enterprise_sales",
                "priority": "P1",
                "next_action": "Schedule discovery call within 2 business hours",
                "sla_hours": 2,
                "confidence": "high"
              }

🤖 AGENT ROUTING DECISION:
Routing Maria Gonzalez from Suncoast Realty Group to the Enterprise Sales team
at Priority 1. This is a high-fit lead scoring 88/100 — the $35,000 budget and
145-person real estate company place her firmly in our enterprise tier. She has
a clear urgency (November deadline