← Back to Blog

What You'll Build

If you've ever watched a sales team drown in unqualified leads or seen a hot prospect go cold because nobody followed up in time, this tutorial is for you. You're going to build a production-ready multi-agent system using the Claude API that automatically scores, qualifies, and prioritizes incoming leads — without a human touching them first.

By the end, you'll have three coordinated AI agents: one that qualifies leads against your criteria, one that scores and ranks them, and an orchestrator that routes everything and returns a clean priority list. The whole thing runs in Python using the Anthropic SDK.

📦 Full Source Code
The complete, working code for this system is built up piece by piece in the steps below. Every snippet is syntactically correct and ready to run. Copy the sections in order and you'll have a working multi-agent lead scoring pipeline by Step 6.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and async concepts
  • anthropic and python-dotenv packages installed
  • A terminal and a code editor you're comfortable with

Step 1: Set Up Your Claude API Environment & Dependencies

Start by creating a project folder and installing the two packages you actually need. I keep this dead simple — no heavyweight frameworks, just the Anthropic SDK and dotenv for keeping your API key out of your source code.

terminal
mkdir lead-scoring-agents
cd lead-scoring-agents
pip install anthropic python-dotenv

Create a .env file in your project root and drop your API key in there. Never hardcode it — future you will thank present you.

.env
ANTHROPIC_API_KEY=sk-ant-your-key-here

Now create your main Python file. This is where everything lives. Here's the base setup with imports and environment loading:

lead_scoring_agents.py
import os
import json
from typing import Any
from dotenv import load_dotenv
import anthropic

load_dotenv()

client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

# Model used for all agents — routing to claude-sonnet-4-6
MODEL = "claude-sonnet-4-6"
⚠️ Environment Check
Run python -c "import anthropic; print(anthropic.__version__)" before you go further. If you see an import error, your install didn't complete. Try pip install --upgrade anthropic.

Step 2: Define Your Lead Scoring Tools & Criteria

The Claude API supports tool use, which lets your agents call structured functions with validated inputs. This is the backbone of the whole system — instead of parsing free-form text, each agent calls a tool and gets back clean, structured data.

I'm defining three tools here: one for qualifying a lead, one for scoring it numerically, and one for flagging high-priority outliers. These are the tool definitions that go into your Claude API calls.

lead_scoring_agents.py (continued)
# Tool definitions for lead qualification and scoring
QUALIFICATION_TOOLS = [
    {
        "name": "qualify_lead",
        "description": (
            "Analyze a lead's information and determine whether they meet "
            "the qualification criteria for sales follow-up. Check budget, "
            "authority, need, and timeline (BANT framework)."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_id": {
                    "type": "string",
                    "description": "Unique identifier for the lead"
                },
                "is_qualified": {
                    "type": "boolean",
                    "description": "True if lead meets minimum qualification criteria"
                },
                "disqualification_reason": {
                    "type": "string",
                    "description": "Reason for disqualification if is_qualified is False, else empty string"
                },
                "bant_scores": {
                    "type": "object",
                    "description": "BANT sub-scores from 0-10",
                    "properties": {
                        "budget": {"type": "number"},
                        "authority": {"type": "number"},
                        "need": {"type": "number"},
                        "timeline": {"type": "number"}
                    },
                    "required": ["budget", "authority", "need", "timeline"]
                },
                "qualification_notes": {
                    "type": "string",
                    "description": "Brief notes explaining the qualification decision"
                }
            },
            "required": [
                "lead_id", "is_qualified", "disqualification_reason",
                "bant_scores", "qualification_notes"
            ]
        }
    }
]

SCORING_TOOLS = [
    {
        "name": "score_lead",
        "description": (
            "Assign a final numeric priority score to a qualified lead "
            "based on their BANT scores, company size, industry fit, "
            "and engagement signals. Score range is 0-100."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_id": {
                    "type": "string",
                    "description": "Unique identifier for the lead"
                },
                "priority_score": {
                    "type": "number",
                    "description": "Final priority score from 0 to 100"
                },
                "priority_tier": {
                    "type": "string",
                    "enum": ["HOT", "WARM", "COLD"],
                    "description": "Priority tier based on score: HOT=80-100, WARM=50-79, COLD=0-49"
                },
                "recommended_action": {
                    "type": "string",
                    "description": "Specific next action for the sales team to take"
                },
                "score_breakdown": {
                    "type": "object",
                    "description": "Breakdown of how the score was calculated",
                    "properties": {
                        "bant_component": {"type": "number"},
                        "company_fit_component": {"type": "number"},
                        "engagement_component": {"type": "number"}
                    },
                    "required": ["bant_component", "company_fit_component", "engagement_component"]
                }
            },
            "required": [
                "lead_id", "priority_score", "priority_tier",
                "recommended_action", "score_breakdown"
            ]
        }
    }
]

Step 3: Build the Qualification Agent with Tool Use

The qualification agent takes raw lead data and runs it through the BANT framework using Claude. The key pattern here is the agent run loop — you send the message, check if Claude wants to use a tool, process that tool call, and feed the result back.

This loop is how every tool-using Claude agent works. Get comfortable with it because it's the same pattern in Step 4 too.

lead_scoring_agents.py (continued)
def run_qualification_agent(lead: dict[str, Any]) -> dict[str, Any]:
    """
    Runs the qualification agent for a single lead.
    Returns structured qualification data from the qualify_lead tool.
    """
    system_prompt = """You are a B2B sales qualification specialist. 
    Analyze the provided lead information using the BANT framework 
    (Budget, Authority, Need, Timeline). Be realistic and critical — 
    not every lead qualifies. Use the qualify_lead tool to return 
    your structured assessment."""

    user_message = f"""Please qualify this lead using the BANT framework:

Lead ID: {lead['lead_id']}
Name: {lead['name']}
Company: {lead['company']}
Title: {lead['title']}
Company Size: {lead['company_size']} employees
Industry: {lead['industry']}
Annual Revenue: ${lead['annual_revenue']:,}
Pain Point: {lead['pain_point']}
Timeline: {lead['timeline']}
Budget Mentioned: {lead['budget_mentioned']}
Engagement: {lead['engagement']}

Assess their budget capacity, decision-making authority, business need, 
and purchase timeline. Then call the qualify_lead tool with your findings."""

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

    # Agent run loop — keep going until Claude stops calling tools
    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            system=system_prompt,
            tools=QUALIFICATION_TOOLS,
            messages=messages
        )

        # Check if Claude wants to use a tool
        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use" and block.name == "qualify_lead":
                    qualification_data = block.input
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps({"status": "success", "data": qualification_data})
                    })

            # Add assistant response and tool results to message history
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

        elif response.stop_reason == "end_turn":
            # Claude finished — extract the last tool call data from history
            for message in reversed(messages):
                if message["role"] == "assistant":
                    for block in message["content"]:
                        if hasattr(block, "name") and block.name == "qualify_lead":
                            return block.input
            break

    return {}  # Fallback if no tool was called
💡 Why the while loop?
Claude might call multiple tools in sequence before finishing. The while True loop with stop_reason checks handles that gracefully. When stop_reason is "end_turn", you know Claude is done and you can pull your results.

Step 4: Implement the Prioritization Agent

Once a lead is qualified, the prioritization agent takes that qualification data plus the original lead info and produces a final 0-100 score. This agent knows about company fit and engagement signals, not just BANT — it gives you a richer ranking.

Notice the pattern is identical to the qualification agent. That's intentional. When all your agents share the same run loop structure, they're easy to debug and extend.

lead_scoring_agents.py (continued)
def run_scoring_agent(lead: dict[str, Any], qualification: dict[str, Any]) -> dict[str, Any]:
    """
    Runs the prioritization agent for a qualified lead.
    Returns structured scoring data including priority tier and recommended action.
    """
    system_prompt = """You are a sales prioritization expert. Given a qualified lead 
    and their BANT assessment, assign a final priority score from 0-100 and 
    recommend the specific next action the sales team should take. 
    HOT leads (80-100) need same-day outreach. WARM leads (50-79) need 
    follow-up within 48 hours. COLD leads (0-49) go into a nurture sequence.
    Use the score_lead tool to return your assessment."""

    bant_scores = qualification.get("bant_scores", {})
    bant_avg = sum(bant_scores.values()) / len(bant_scores) if bant_scores else 0

    user_message = f"""Score and prioritize this qualified lead:

Lead ID: {lead['lead_id']}
Company: {lead['company']} ({lead['company_size']} employees)
Industry: {lead['industry']}
Annual Revenue: ${lead['annual_revenue']:,}
Engagement Level: {lead['engagement']}
Timeline: {lead['timeline']}

BANT Assessment:
- Budget Score: {bant_scores.get('budget', 0)}/10
- Authority Score: {bant_scores.get('authority', 0)}/10
- Need Score: {bant_scores.get('need', 0)}/10
- Timeline Score: {bant_scores.get('timeline', 0)}/10
- Average BANT Score: {bant_avg:.1f}/10

Qualification Notes: {qualification.get('qualification_notes', '')}

Our ideal customer profile: Mid-market companies (50-500 employees), 
technology-forward industries, active pain points, budget > $50k/year.

Call the score_lead tool with your final priority score, tier, and recommended action."""

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

    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            system=system_prompt,
            tools=SCORING_TOOLS,
            messages=messages
        )

        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use" and block.name == "score_lead":
                    scoring_data = block.input
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps({"status": "success", "data": scoring_data})
                    })

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

        elif response.stop_reason == "end_turn":
            for message in reversed(messages):
                if message["role"] == "assistant":
                    for block in message["content"]:
                        if hasattr(block, "name") and block.name == "score_lead":
                            return block.input
            break

    return {}

Step 5: Create the Orchestrator Agent to Coordinate Tasks

The orchestrator is what turns two individual agents into a real system. It loops through every lead, calls the qualification agent first, skips disqualified leads, then routes qualified ones to the scoring agent. Finally it sorts everything into a priority-ranked output.

This is the main orchestrator agent class. It's also where you'd add logging, error handling, or a database write in a production version.

lead_scoring_agents.py (continued)
class LeadScoringOrchestrator:
    """
    Orchestrates the multi-agent lead scoring pipeline.
    Coordinates qualification and prioritization agents across a batch of leads.
    """

    def __init__(self, leads: list[dict[str, Any]]):
        self.leads = leads
        self.results = []

    def process_batch(self) -> list[dict[str, Any]]:
        """Process all leads through the full qualification + scoring pipeline."""
        print(f"\n{'='*60}")
        print(f"Starting lead scoring pipeline for {len(self.leads)} leads")
        print(f"{'='*60}\n")

        for i, lead in enumerate(self.leads, 1):
            print(f"[{i}/{len(self.leads)}] Processing: {lead['name']} at {lead['company']}...")

            # Stage 1: Qualification
            qualification = run_qualification_agent(lead)

            if not qualification:
                print(f"  ⚠️  Qualification agent returned no data for {lead['lead_id']}")
                continue

            if not qualification.get("is_qualified", False):
                reason = qualification.get("disqualification_reason", "Unknown")
                print(f"  ✗  Disqualified: {reason}")
                self.results.append({
                    "lead_id": lead["lead_id"],
                    "name": lead["name"],
                    "company": lead["company"],
                    "status": "DISQUALIFIED",
                    "priority_score": 0,
                    "priority_tier": "DISQUALIFIED",
                    "disqualification_reason": reason,
                    "recommended_action": "Do not contact — does not meet criteria",
                    "qualification": qualification,
                    "scoring": None
                })
                continue

            print(f"  ✓  Qualified — running scoring agent...")

            # Stage 2: Scoring (only for qualified leads)
            scoring = run_scoring_agent(lead, qualification)

            if not scoring:
                print(f"  ⚠️  Scoring agent returned no data for {lead['lead_id']}")
                continue

            tier = scoring.get("priority_tier", "COLD")
            score = scoring.get("priority_score", 0)
            print(f"  🎯 Score: {score}/100 | Tier: {tier}")

            self.results.append({
                "lead_id": lead["lead_id"],
                "name": lead["name"],
                "company": lead["company"],
                "status": "QUALIFIED",
                "priority_score": score,
                "priority_tier": tier,
                "disqualification_reason": "",
                "recommended_action": scoring.get("recommended_action", ""),
                "qualification": qualification,
                "scoring": scoring
            })

        # Sort qualified leads by priority score descending
        self.results.sort(key=lambda x: x["priority_score"], reverse=True)
        return self.results

    def print_summary(self) -> None:
        """Print a formatted summary of the scoring results."""
        qualified = [r for r in self.results if r["status"] == "QUALIFIED"]
        disqualified = [r for r in self.results if r["status"] == "DISQUALIFIED"]

        print(f"\n{'='*60}")
        print("LEAD SCORING RESULTS SUMMARY")
        print(f"{'='*60}")
        print(f"Total Processed: {len(self.results)}")
        print(f"Qualified:       {len(qualified)}")
        print(f"Disqualified:    {len(disqualified)}")
        print(f"{'='*60}\n")

        tiers = {"HOT": [], "WARM": [], "COLD": []}
        for lead in qualified:
            tier = lead["priority_tier"]
            if tier in tiers:
                tiers[tier].append(lead)

        for tier, leads in tiers.items():
            if leads:
                icon = {"HOT": "🔥", "WARM": "♨️", "COLD": "❄️"}[tier]
                print(f"{icon} {tier} LEADS ({len(leads)})")
                print("-" * 40)
                for lead in leads:
                    print(f"  [{lead['priority_score']:.0f}/100] {lead['name']} — {lead['company']}")
                    print(f"  → {lead['recommended_action']}")
                    print()

        if disqualified:
            print(f"✗ DISQUALIFIED ({len(disqualified)})")
            print("-" * 40)
            for lead in disqualified:
                print(f"  {lead['name']} — {lead['company']}")
                print(f"  Reason: {lead['disqualification_reason']}")
                print()

Step 6: Test with Real Lead Data

Here's where it all comes together. I've put together a realistic batch of five leads — some that should score hot, some that should wash out. Run this and you'll immediately see how well the agents are reasoning about your criteria.

lead_scoring_agents.py (continued)
def main():
    # Sample leads representing a realistic mix of quality
    sample_leads = [
        {
            "lead_id": "LEAD-001",
            "name": "Sarah Chen",
            "company": "TechFlow Solutions",
            "title": "Chief Operations Officer",
            "company_size": 220,
            "industry": "SaaS / Technology",
            "annual_revenue": 18_000_000,
            "pain_point": "Manual data entry consuming 40 hours/week across ops team",
            "timeline": "Looking to implement within 60 days",
            "budget_mentioned": "$80,000 annual budget approved",
            "engagement": "Attended two webinars, downloaded ROI calculator, replied to cold email"
        },
        {
            "lead_id": "LEAD-002",
            "name": "Marcus Rivera",
            "company": "Gulf Coast Auto Group",
            "title": "General Manager",
            "company_size": 85,
            "industry": "Automotive Dealerships",
            "annual_revenue": 24_000_000,
            "pain_point": "Losing leads because sales staff can't follow up fast enough",
            "timeline": "Needs solution before Q3 starts",
            "budget_mentioned": "Has budget but hasn't confirmed amount",
            "engagement": "Clicked 3 emails, visited pricing page twice"
        },
        {
            "lead_id": "LEAD-003",
            "name": "Jennifer Walsh",
            "company": "Walsh Family Bakery",
            "title": "Owner",
            "company_size": 4,
            "industry": "Food & Beverage (Retail)",
            "annual_revenue": 280_000,
            "pain_point": "Wants to grow social media presence",
            "timeline": "Someday, no rush",
            "budget_mentioned": "Looking for something cheap or free",
            "engagement": "Filled out contact form once"
        },
        {
            "lead_id": "LEAD-004",
            "name": "Dr. Patricia Monroe",
            "company": "