All articles

What You'll Build

If you've ever had your sales team waste hours chasing leads that never convert, this tutorial solves that problem. You'll build a working lead scoring agent in Python that uses Claude's tool use feature to automatically evaluate inbound leads and assign them a score from 0 to 100, along with a qualification tier and a recommended next action. By the end, you'll have production-ready code you can drop into any CRM pipeline or sales workflow.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and dictionaries
  • anthropic Python SDK installed (pip install anthropic)
  • A terminal and a code editor you're comfortable with
Full Source Code Note: The complete, working code for this lead scoring agent is built up step by step in the sections below. Each snippet is self-contained enough to follow along, and the final agent class in Step 3 ties everything together into one runnable script. You do not need any external dependencies beyond the Anthropic SDK.

Step 1: Set Up Your Claude API Environment

First things first, let's get the environment wired up. You want to keep your API key out of your source code, so we'll pull it from an environment variable instead of hardcoding it.

Run this in your terminal to install the SDK if you haven't already:

terminal
pip install anthropic

Then set your API key as an environment variable:

terminal (Mac/Linux)
export ANTHROPIC_API_KEY="sk-ant-your-key-here"
terminal (Windows)
set ANTHROPIC_API_KEY=sk-ant-your-key-here

Now let's verify the client initializes correctly before we write any agent logic:

test_connection.py
import os
import anthropic

# Initialize the client using the environment variable automatically
client = anthropic.Anthropic()

# Quick ping to confirm the API key works
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=64,
    messages=[{"role": "user", "content": "Reply with: API connection successful."}]
)

print(response.content[0].text)

You should see API connection successful. printed in your terminal. If you get an authentication error, double-check that the environment variable is set in the same shell session you're running Python from.

Step 2: Define the Lead Scoring Tool and Criteria

This is the core of how Claude's tool use works. You define a tool as a JSON schema that describes what inputs the model can pass back to your Python code. Think of it like giving Claude a structured form to fill out instead of letting it reply with freeform text.

For lead scoring, we want Claude to evaluate five dimensions: budget fit, authority (is this person a decision-maker?), need, timeline, and company size. Each one gets a weighted sub-score, and the tool returns a combined result.

lead_scoring_tools.py
import anthropic

# The tool definition tells Claude exactly what fields to return
LEAD_SCORING_TOOL = {
    "name": "score_lead",
    "description": (
        "Evaluate a sales lead and return a structured score based on budget fit, "
        "decision-making authority, business need, purchase timeline, and company size. "
        "Use the lead information provided to fill out every field accurately."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "score": {
                "type": "integer",
                "description": "Overall lead score from 0 to 100. Higher means stronger fit.",
                "minimum": 0,
                "maximum": 100
            },
            "tier": {
                "type": "string",
                "enum": ["Hot", "Warm", "Cold", "Disqualified"],
                "description": "Qualification tier based on overall score."
            },
            "budget_score": {
                "type": "integer",
                "description": "Budget alignment score from 0 to 25.",
                "minimum": 0,
                "maximum": 25
            },
            "authority_score": {
                "type": "integer",
                "description": "Decision-making authority score from 0 to 25.",
                "minimum": 0,
                "maximum": 25
            },
            "need_score": {
                "type": "integer",
                "description": "Strength of business need score from 0 to 25.",
                "minimum": 0,
                "maximum": 25
            },
            "timeline_score": {
                "type": "integer",
                "description": "Purchase timeline urgency score from 0 to 25.",
                "minimum": 0,
                "maximum": 25
            },
            "reasoning": {
                "type": "string",
                "description": "2-3 sentence plain-English explanation of why this lead received this score."
            },
            "recommended_action": {
                "type": "string",
                "description": "The single best next action for the sales team to take with this lead."
            }
        },
        "required": [
            "score", "tier", "budget_score", "authority_score",
            "need_score", "timeline_score", "reasoning", "recommended_action"
        ]
    }
}
Why BANT? Budget, Authority, Need, and Timeline (BANT) is a classic sales qualification framework. We've weighted each dimension equally at 25 points max so the math stays simple. You can adjust those weights for your specific sales cycle once the agent is running.

Step 3: Create the Agent with Tool Use

Now we build the main agent class. This is where the real work happens. The agent takes a lead, sends it to Claude with the scoring tool attached, and then processes whatever Claude decides to do, whether that's calling the tool directly or asking for clarification first.

The system prompt matters a lot here. You want Claude to act like a senior sales analyst who has seen thousands of leads, not like a generic assistant.

lead_scoring_agent.py
import os
import json
import anthropic
from lead_scoring_tools import LEAD_SCORING_TOOL

SYSTEM_PROMPT = """You are a senior B2B sales analyst with 15 years of experience 
qualifying inbound leads. Your job is to evaluate each lead objectively using the 
score_lead tool. Always call the tool — never reply with plain text. Base your 
scoring strictly on the information provided. If a field is missing, score it 
conservatively (low). Do not inflate scores to be encouraging."""

class LeadScoringAgent:
    def __init__(self):
        # Client picks up ANTHROPIC_API_KEY from the environment automatically
        self.client = anthropic.Anthropic()
        self.model = "claude-sonnet-4-6"
        self.tools = [LEAD_SCORING_TOOL]

    def _format_lead_message(self, lead: dict) -> str:
        """Convert a lead dictionary into a readable message for Claude."""
        lines = ["Please score the following sales lead:"]
        field_labels = {
            "name": "Name",
            "company": "Company",
            "title": "Job Title",
            "email": "Email",
            "budget": "Stated Budget",
            "timeline": "Purchase Timeline",
            "employees": "Company Size (employees)",
            "industry": "Industry",
            "pain_points": "Stated Pain Points",
            "source": "Lead Source",
            "notes": "Additional Notes"
        }
        for key, label in field_labels.items():
            if key in lead:
                lines.append(f"{label}: {lead[key]}")
        return "\n".join(lines)

    def score_lead(self, lead: dict) -> dict:
        """
        Send a lead to Claude and return the structured scoring result.
        Raises ValueError if Claude does not call the scoring tool.
        """
        user_message = self._format_lead_message(lead)

        response = self.client.messages.create(
            model=self.model,
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            tools=self.tools,
            messages=[{"role": "user", "content": user_message}]
        )

        # Check why Claude stopped generating
        if response.stop_reason == "tool_use":
            # Find the tool_use block in the response content
            for block in response.content:
                if block.type == "tool_use" and block.name == "score_lead":
                    return block.input  # This is already a parsed dict

        raise ValueError(
            f"Claude did not call the score_lead tool. "
            f"Stop reason: {response.stop_reason}. "
            f"Response: {response.content}"
        )

Step 4: Build the Run Loop and Process Leads in Bulk

A single lead scoring call is useful, but the real value comes from processing a batch of leads and getting a ranked list back. Let's build a run loop that takes a list of leads, scores each one, and sorts the results by score so your sales team knows exactly who to call first.

We'll also add basic error handling so one bad lead doesn't crash the whole batch.

run_scoring.py
import json
import anthropic
from lead_scoring_agent import LeadScoringAgent

def process_leads(leads: list[dict]) -> list[dict]:
    """
    Score a list of leads and return them sorted by score (highest first).
    Each result dict contains the original lead data merged with scoring output.
    """
    agent = LeadScoringAgent()
    results = []

    for i, lead in enumerate(leads):
        print(f"Scoring lead {i + 1}/{len(leads)}: {lead.get('name', 'Unknown')}...")
        try:
            score_result = agent.score_lead(lead)
            # Merge original lead info with the scoring output
            combined = {**lead, **score_result}
            results.append(combined)
        except ValueError as e:
            print(f"  Warning: Could not score lead {lead.get('name')}. Error: {e}")
            # Append with a placeholder so we don't lose the lead record
            results.append({**lead, "score": 0, "tier": "Error", "reasoning": str(e)})

    # Sort by score descending so the hottest leads appear first
    results.sort(key=lambda x: x.get("score", 0), reverse=True)
    return results

def print_results(results: list[dict]) -> None:
    """Print a clean summary of scored leads to the console."""
    print("\n" + "=" * 60)
    print("LEAD SCORING RESULTS (Ranked by Score)")
    print("=" * 60)

    for lead in results:
        print(f"\nName:       {lead.get('name', 'N/A')}")
        print(f"Company:    {lead.get('company', 'N/A')}")
        print(f"Score:      {lead.get('score', 0)}/100  |  Tier: {lead.get('tier', 'N/A')}")
        print(f"Reasoning:  {lead.get('reasoning', 'N/A')}")
        print(f"Next Action: {lead.get('recommended_action', 'N/A')}")
        print(f"Sub-scores: Budget={lead.get('budget_score', 0)} | "
              f"Authority={lead.get('authority_score', 0)} | "
              f"Need={lead.get('need_score', 0)} | "
              f"Timeline={lead.get('timeline_score', 0)}")
        print("-" * 60)

if __name__ == "__main__":
    # Import the sample leads defined in the next step
    from sample_leads import SAMPLE_LEADS

    results = process_leads(SAMPLE_LEADS)
    print_results(results)

    # Optionally save results to JSON for downstream CRM import
    with open("scored_leads.json", "w") as f:
        json.dump(results, f, indent=2)
    print("\nResults saved to scored_leads.json")

Step 5: Test with Real Lead Data

Let's use three realistic leads that represent the range you'd actually see: one strong prospect, one borderline case, and one that should be disqualified early. This gives you a good smoke test for whether your scoring criteria are calibrated correctly.

sample_leads.py
# Realistic sample leads for testing the scoring agent

SAMPLE_LEADS = [
    {
        "name": "Maria Gonzalez",
        "company": "Gulf Coast Properties LLC",
        "title": "CEO",
        "email": "[email protected]",
        "budget": "$15,000 - $25,000 for AI automation tools",
        "timeline": "Want to launch before Q1 next year (90 days)",
        "employees": "45",
        "industry": "Real Estate",
        "pain_points": (
            "Manually entering MLS listings takes 3 hours per agent per week. "
            "Lead follow-up is inconsistent. No CRM automation in place."
        ),
        "source": "Website contact form",
        "notes": "Already approved budget with board. Comparing 2 vendors."
    },
    {
        "name": "Derek Thompson",
        "company": "Thompson Auto Group",
        "title": "Marketing Coordinator",
        "email": "[email protected]",
        "budget": "Not sure yet, maybe a few thousand",
        "timeline": "Sometime next year, no rush",
        "employees": "120",
        "industry": "Car Dealership",
        "pain_points": "Our website doesn't rank well and we're losing leads to competitors.",
        "source": "LinkedIn ad click",
        "notes": "Said he'd need to get his GM involved before any decisions."
    },
    {
        "name": "anonymous",
        "company": "Unknown",
        "title": "Not provided",
        "email": "[email protected]",
        "budget": "Free or very cheap",
        "timeline": "Just browsing",
        "employees": "1",
        "industry": "Unknown",
        "pain_points": "General curiosity about AI",
        "source": "Blog comment",
        "notes": "No phone number provided. Email appears to be temporary."
    }
]

Run the full pipeline with:

terminal
python run_scoring.py

Here's what the actual output looks like when you run this against Claude:

sample output
Scoring lead 1/3: Maria Gonzalez...
Scoring lead 2/3: Derek Thompson...
Scoring lead 3/3: anonymous...

============================================================
LEAD SCORING RESULTS (Ranked by Score)
============================================================

Name:       Maria Gonzalez
Company:    Gulf Coast Properties LLC
Score:      88/100  |  Tier: Hot
Reasoning:  Maria is the CEO with board-approved budget in the $15K-$25K range and a 
            concrete 90-day timeline, which indicates serious purchase intent. The pain 
            points are specific and directly addressable. The only risk is active 
            vendor comparison, so speed of follow-up is critical.
Next Action: Schedule a discovery call within 24 hours and send a capability overview 
             deck focused on real estate listing automation.
Sub-scores: Budget=22 | Authority=25 | Need=22 | Timeline=19
------------------------------------------------------------

Name:       Derek Thompson
Company:    Thompson Auto Group
Score:      38/100  |  Tier: Cold
Reasoning:  Derek is a marketing coordinator without purchasing authority, has no 
            defined budget, and has a vague timeline. However, the company is a 
            legitimate 120-person dealership with a real problem. The opportunity 
            exists if he can connect us to the GM.
Next Action: Send a low-pressure email with a case study relevant to auto dealerships 
             and ask if he can loop in the GM for a 20-minute call.
Sub-scores: Budget=5 | Authority=8 | Need=15 | Timeline=10
------------------------------------------------------------

Name:       anonymous
Company:    Unknown
Score:      3/100  |  Tier: Disqualified
Reasoning:  No identifying information, a temporary email address, no budget, and 
            no stated need beyond general curiosity. There is no viable sales 
            opportunity here at this time.
Next Action: Add to a nurture email sequence only. Do not assign to a sales rep.
Sub-scores: Budget=1 | Authority=1 | Need=1 | Timeline=0
------------------------------------------------------------

Results saved to scored_leads.json

How It Works

Here's the plain-English version of what's happening under the hood. When you call agent.score_lead(lead), the agent formats the lead as a natural language message and sends it to Claude along with the tool definition. Claude reads the lead, reasons about it internally, and then decides to "call" the score_lead tool by returning a structured JSON payload that matches your schema.

Your Python code intercepts that tool call, pulls out the block.input dict, and that's your score. Claude never replies with freeform text in this flow because the system prompt instructs it to always use the tool, and the tool schema gives it a clear structure to fill out.

The run loop in run_scoring.py just iterates over your lead list, calls the agent for each one, catches any errors gracefully, and sorts everything by score. The output is a plain Python list of dicts you can pipe into a database, a CRM webhook, or a CSV export with a few extra lines.

On latency: Each lead scoring call typically takes 2 to 4 seconds with Claude Sonnet. For large batches, consider adding asyncio and making concurrent API calls to speed things up. The Anthropic SDK supports async with anthropic.AsyncAnthropic().

Common Errors and Fixes

Error 1: AuthenticationError on startup

anthropic.AuthenticationError: {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}

Fix: Your API key isn't being picked up from the environment. Run echo $ANTHROPIC_API_KEY in your terminal to confirm it's set. If the variable is empty, re-export it in the same terminal session where you're running Python. You can also pass the key directly as anthropic.Anthropic(api_key="sk-ant-...") for debugging, but don't commit that to source control.

Error 2: ValueError because Claude returned text instead of a tool call

ValueError: Claude did not call the score_lead tool. Stop reason: end_turn.

Fix: This usually means the system prompt wasn't strong enough to force tool use, or the lead message was so short that Claude responded conversationally. Make sure your system prompt explicitly says "Always call the tool, never reply with plain text." You can also set tool_choice={"type": "tool", "name": "score_lead"} in your messages.create() call to force Claude to use that specific tool every time.

Error 3: KeyError when accessing score results

KeyError: 'budget_score'

Fix: The tool schema has budget_score in the required array, so this shouldn't happen unless you modified the schema and forgot to update the required list. Check that every field you access in print_results() is listed under "required" in your tool's input_schema. Using .get("budget_score", 0) is also a safe fallback for optional fields.

Next Steps

  • Connect to your CRM via webhook. Post the scored lead JSON to HubSpot, Salesforce, or GoHighLevel using the requests library so scores appear automatically inside your existing workflow.
  • Add async processing for high-volume batches. Swap anthropic.Anthropic() for anthropic.AsyncAnthropic() and use asyncio.gather() to score dozens of leads in parallel instead of one at a time.
  • Customize scoring weights per industry. A real estate lead and a manufacturing lead shouldn't be scored the same way. Pass industry-specific scoring instructions into the system prompt dynamically based on the lead's industry field.
  • Log scores to a database and track accuracy. Store every score in a Postgres or SQLite table alongside the eventual deal outcome so you can measure whether the agent's predictions match reality over time and tune the criteria accordingly.

Frequently Asked Questions

How does Claude tool