All articles

What You'll Build

If you've ever wasted time chasing leads that were never going to close, this tutorial is for you. You're going to build a working lead qualification agent in Python that uses Claude's tool-use API to automatically score leads, check their budget fit, and validate their project timeline. By the end, you'll have a production-ready script that tells you whether a lead is worth pursuing, all in under 100 lines of code.

Full Source Code Note: The complete, working code for this lead qualification agent is broken into steps below so you can understand what each piece does. If you want to copy the whole thing at once, all snippets are self-contained and designed to be assembled in order. Every code block is syntactically correct and ready to run.

Prerequisites

  • Python 3.9 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python functions and classes
  • The anthropic package installed (pip install anthropic)
  • A text editor or IDE like VS Code

Step 1: Set Up Claude SDK and Authentication

Start by installing the Anthropic SDK if you haven't already. Open your terminal and run pip install anthropic. Then set your API key as an environment variable so you're not hardcoding secrets into your script.

terminal
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-your-key-here"

Now create a new file called lead_qualifier.py and start with the imports and client setup. The SDK picks up your API key from the environment automatically, which keeps things clean.

lead_qualifier.py
import os
import json
import anthropic

# Initialize the Anthropic client using ANTHROPIC_API_KEY from environment
client = anthropic.Anthropic()

MODEL = "claude-sonnet-4-6"
Tip: Never paste your API key directly into your Python file. Use environment variables or a .env file with the python-dotenv package. If you accidentally push your key to GitHub, rotate it immediately from the Anthropic console.

Step 2: Define Lead Qualification Tools

This is where the agent gets its power. Claude's tool-use feature lets you define functions that the model can decide to call on its own based on the conversation. You're going to define three tools: score_lead, check_budget, and validate_timeline.

Each tool definition is a dictionary that tells Claude what the function does, what parameters it expects, and which parameters are required. Think of it as writing a contract between your code and the model.

lead_qualifier.py
import os
import json
import anthropic

client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"

# Tool definitions tell Claude what functions it can call
TOOLS = [
    {
        "name": "score_lead",
        "description": (
            "Scores a lead from 0 to 100 based on company size, industry fit, "
            "and expressed pain points. Returns a numeric score and a short rationale."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "company_name": {
                    "type": "string",
                    "description": "Name of the prospect company"
                },
                "industry": {
                    "type": "string",
                    "description": "Industry the company operates in"
                },
                "employee_count": {
                    "type": "integer",
                    "description": "Number of employees at the company"
                },
                "pain_points": {
                    "type": "string",
                    "description": "Described business problems or needs"
                }
            },
            "required": ["company_name", "industry", "employee_count", "pain_points"]
        }
    },
    {
        "name": "check_budget",
        "description": (
            "Checks whether the lead's stated budget meets the minimum threshold "
            "for a viable engagement. Returns a fit assessment."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "stated_budget": {
                    "type": "number",
                    "description": "Budget the prospect has stated in USD"
                },
                "minimum_viable_budget": {
                    "type": "number",
                    "description": "Minimum budget required to deliver the solution"
                }
            },
            "required": ["stated_budget", "minimum_viable_budget"]
        }
    },
    {
        "name": "validate_timeline",
        "description": (
            "Validates whether the lead's desired timeline is realistic given "
            "typical project delivery windows. Returns a timeline assessment."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "desired_start_weeks": {
                    "type": "integer",
                    "description": "How many weeks from now the prospect wants to start"
                },
                "desired_completion_weeks": {
                    "type": "integer",
                    "description": "How many weeks from now the prospect wants the project done"
                }
            },
            "required": ["desired_start_weeks", "desired_completion_weeks"]
        }
    }
]

Step 3: Build the Agent Logic with Tool Use

Now you'll write the core agent loop. This is the part that keeps calling Claude, processes whatever tools it wants to use, and feeds the results back in until it's done reasoning. This pattern is called an agentic loop, and it's the backbone of how to build AI agents that actually do work.

The loop runs until Claude returns a stop_reason of "end_turn", which means it's finished thinking and has a final answer for you. Every tool call Claude makes gets intercepted by your Python code, executed with real logic, and returned as a result.

lead_qualifier.py
def run_qualification_agent(lead_info: str) -> str:
    """
    Runs the lead qualification agent for a given lead description.
    Returns the agent's final qualification verdict as a string.
    """
    messages = [
        {
            "role": "user",
            "content": (
                f"Please qualify this lead and give me a clear verdict on whether "
                f"we should pursue them. Here is the lead information:\n\n{lead_info}"
            )
        }
    ]

    system_prompt = (
        "You are a lead qualification specialist for a custom AI solutions agency "
        "in Southwest Florida. Your job is to evaluate leads and determine whether "
        "they are worth pursuing. Use the available tools to score the lead, check "
        "their budget fit, and validate their timeline. After using all relevant "
        "tools, give a final clear verdict: QUALIFIED or NOT QUALIFIED, with a "
        "brief explanation a salesperson can act on immediately."
    )

    # Agentic loop: keep going until Claude signals it is done
    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            system=system_prompt,
            tools=TOOLS,
            messages=messages
        )

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

        if response.stop_reason == "end_turn":
            # Extract the final text response from the content blocks
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text
            return "Agent finished without a text response."

        if response.stop_reason == "tool_use":
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    tool_name = block.name
                    tool_input = block.input
                    tool_result = execute_tool(tool_name, tool_input)

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(tool_result)
                    })

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

        else:
            # Unexpected stop reason, exit the loop
            break

    return "Agent loop exited unexpectedly."

Step 4: Create the Lead Scoring Function

Now you need the actual Python functions that execute when Claude calls a tool. These are your real business logic layers. Claude decides when to call them and with what arguments, but your code controls what actually happens.

The execute_tool dispatcher routes each tool call to the right function. Each qualification function uses straightforward logic that you can customize for your own business rules.

lead_qualifier.py
def score_lead(
    company_name: str,
    industry: str,
    employee_count: int,
    pain_points: str
) -> dict:
    """Scores a lead from 0-100 based on fit criteria."""

    score = 0
    rationale_parts = []

    # Industry fit scoring
    high_fit_industries = ["real estate", "healthcare", "manufacturing", "automotive"]
    medium_fit_industries = ["restaurant", "retail", "logistics"]

    industry_lower = industry.lower()
    if any(ind in industry_lower for ind in high_fit_industries):
        score += 40
        rationale_parts.append(f"{industry} is a high-fit industry (+40)")
    elif any(ind in industry_lower for ind in medium_fit_industries):
        score += 25
        rationale_parts.append(f"{industry} is a medium-fit industry (+25)")
    else:
        score += 10
        rationale_parts.append(f"{industry} is a lower-fit industry (+10)")

    # Employee count scoring (sweet spot is 10-500 employees)
    if 10 <= employee_count <= 500:
        score += 30
        rationale_parts.append(f"{employee_count} employees is in the ideal range (+30)")
    elif employee_count > 500:
        score += 20
        rationale_parts.append(f"{employee_count} employees is enterprise-scale (+20)")
    else:
        score += 5
        rationale_parts.append(f"{employee_count} employees may be too small (+5)")

    # Pain point quality scoring
    high_value_keywords = [
        "manual", "automate", "slow", "error", "inefficient",
        "scale", "cost", "time", "repetitive", "data"
    ]
    matched = [kw for kw in high_value_keywords if kw in pain_points.lower()]
    pain_score = min(30, len(matched) * 6)
    score += pain_score
    rationale_parts.append(
        f"Pain points matched {len(matched)} high-value keywords (+{pain_score})"
    )

    return {
        "company": company_name,
        "score": score,
        "max_score": 100,
        "rationale": "; ".join(rationale_parts)
    }


def check_budget(stated_budget: float, minimum_viable_budget: float) -> dict:
    """Checks if the stated budget meets the minimum threshold."""

    if stated_budget >= minimum_viable_budget:
        fit = "BUDGET_FIT"
        message = (
            f"Budget of ${stated_budget:,.0f} meets or exceeds "
            f"the minimum of ${minimum_viable_budget:,.0f}."
        )
    elif stated_budget >= minimum_viable_budget * 0.75:
        fit = "BUDGET_CLOSE"
        message = (
            f"Budget of ${stated_budget:,.0f} is within 25% of the minimum "
            f"(${minimum_viable_budget:,.0f}). Negotiation may be possible."
        )
    else:
        fit = "BUDGET_LOW"
        message = (
            f"Budget of ${stated_budget:,.0f} is too far below "
            f"the minimum of ${minimum_viable_budget:,.0f}."
        )

    return {"fit": fit, "message": message}


def validate_timeline(
    desired_start_weeks: int,
    desired_completion_weeks: int
) -> dict:
    """Validates whether the desired timeline is realistic."""

    project_duration = desired_completion_weeks - desired_start_weeks
    minimum_realistic_weeks = 4  # Minimum weeks for any meaningful AI project

    if desired_start_weeks < 2:
        urgency = "URGENT"
    elif desired_start_weeks <= 8:
        urgency = "NORMAL"
    else:
        urgency = "DELAYED"

    if project_duration >= minimum_realistic_weeks:
        timeline_valid = True
        message = (
            f"Project duration of {project_duration} weeks is realistic. "
            f"Start urgency: {urgency}."
        )
    else:
        timeline_valid = False
        message = (
            f"Project duration of {project_duration} weeks is too compressed. "
            f"Minimum realistic duration is {minimum_realistic_weeks} weeks."
        )

    return {
        "timeline_valid": timeline_valid,
        "project_duration_weeks": project_duration,
        "urgency": urgency,
        "message": message
    }


def execute_tool(tool_name: str, tool_input: dict) -> dict:
    """Routes tool calls from Claude to the correct Python function."""

    if tool_name == "score_lead":
        return score_lead(**tool_input)
    elif tool_name == "check_budget":
        return check_budget(**tool_input)
    elif tool_name == "validate_timeline":
        return validate_timeline(**tool_input)
    else:
        return {"error": f"Unknown tool: {tool_name}"}

Step 5: Run the Agent and Test with Sample Leads

Now let's wire everything together with a main block that tests two sample leads. One should qualify and one shouldn't. This is the most satisfying part because you'll see the agent reason through each one differently.

lead_qualifier.py
if __name__ == "__main__":

    # Lead 1: Strong candidate
    qualified_lead = """
    Company: Coastal Realty Group
    Industry: Real Estate
    Employees: 85
    Pain Points: Our agents manually enter data from MLS into our CRM every day.
    It's slow, error-prone, and eating up 3 hours per agent per week. We also
    want to automate our listing descriptions and follow-up emails.
    Budget: $45,000
    Desired Start: 3 weeks from now
    Desired Completion: 12 weeks from now
    Minimum Budget for Our Solution: $30,000
    """

    # Lead 2: Poor candidate
    unqualified_lead = """
    Company: Marco Island Surf Shop
    Industry: Retail - Specialty Sports
    Employees: 4
    Pain Points: We want to try some AI stuff, not sure what exactly.
    Budget: $2,500
    Desired Start: 1 week from now
    Desired Completion: 2 weeks from now
    Minimum Budget for Our Solution: $15,000
    """

    print("=" * 60)
    print("LEAD 1: Coastal Realty Group")
    print("=" * 60)
    result_1 = run_qualification_agent(qualified_lead)
    print(result_1)

    print("\n" + "=" * 60)
    print("LEAD 2: Marco Island Surf Shop")
    print("=" * 60)
    result_2 = run_qualification_agent(unqualified_lead)
    print(result_2)

Run the script with python lead_qualifier.py and you should see output that looks something like this.

sample output
============================================================
LEAD 1: Coastal Realty Group
============================================================
VERDICT: QUALIFIED

Coastal Realty Group scored 76/100 on lead scoring. Real estate is a
high-fit industry, their team of 85 employees is in the ideal range, and
their pain points around manual data entry, listing automation, and email
follow-up map directly to solutions we deliver.

Budget check passed: $45,000 exceeds the $30,000 minimum. The 9-week
project window (starting in 3 weeks) is realistic and the start urgency
is NORMAL.

Recommendation: Schedule a discovery call this week. Lead is sales-ready.

============================================================
LEAD 2: Marco Island Surf Shop
============================================================
VERDICT: NOT QUALIFIED

Marco Island Surf Shop does not meet qualification criteria at this time.

Lead score was 21/100. With only 4 employees and vague pain points, there
is limited scope for a meaningful AI engagement. Budget of $2,500 is
significantly below the $15,000 minimum (BUDGET_LOW). The requested
timeline of 1 week to completion is also unrealistic for any AI project.

Recommendation: Do not pursue. Flag for a low-cost introductory offer or
nurture sequence if future budget develops.

How It Works

Here's the plain-English version of what's happening under the hood. When you send a lead description to Claude, it reads the system prompt and understands its job is to qualify leads using tools. It then decides, on its own, which tools to call and in what order.

Your Python code intercepts each tool call, runs the actual scoring logic, and sends the results back to Claude in the next message. Claude reads those results and either calls more tools or writes its final verdict. This back-and-forth is the agentic loop, and it keeps running until Claude says it's done.

The reason this works better than just asking Claude to score leads directly is that your business rules live in Python where you can version-control, test, and modify them without touching the prompt. Claude handles reasoning and language; your code handles the numbers.

Common Errors and Fixes

Error 1: AuthenticationError on Startup

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

This means your API key isn't being read correctly. Check that you exported it in the same terminal session where you're running the script. Run echo $ANTHROPIC_API_KEY to verify it's set. If it prints nothing, export it again or add it to your .bashrc or .zshrc file.

Error 2: KeyError When Executing Tools

TypeError: score_lead() got an unexpected keyword argument 'company_size'

This happens when Claude sends a parameter name that doesn't match your function signature. Usually it means your tool description used a different parameter name than your function expects. Double-check that every key in your input_schema properties exactly matches the parameter name in the Python function. They are case-sensitive.

Error 3: Infinite Loop with No End Turn

# Script runs forever and never prints output

This usually means your tool results are being sent back in the wrong format and Claude keeps waiting for valid data. Make sure you're wrapping tool results in the correct structure with "type": "tool_result" and the matching tool_use_id. Also confirm you're calling json.dumps() on the result content before sending it back, as the API expects a string there.

Next Steps

Once this is working locally, here are four ways to make it more powerful.

  • Connect it to a real CRM. Replace the sample lead strings with data pulled from HubSpot, Salesforce, or a Google Sheet using their APIs. The agent logic stays the same.
  • Add a webhook endpoint. Wrap the run_qualification_agent function in a Flask or FastAPI route so it can be triggered by form submissions on your website in real time.
  • Persist results to a database. Write the final verdict and score to a Postgres or SQLite table so you can track qualification trends over time and refine your scoring rules.
  • Add a research_company tool. Use a web search API or scraping tool to automatically pull company data so sales reps don't have to fill in lead forms manually.

FAQ

How does Claude tool use work in Python?

Claude tool use works by passing a list of tool definitions to the API along with your messages. When Claude decides a tool should be called, it returns a response with stop_reason: "tool_use" and includes the tool name and arguments in the content. Your Python code then executes the actual function and sends the result back as a new message. Claude reads the result and continues reasoning until it's ready to give a final answer.

What is an agentic loop in AI development?

An agentic loop is a