All articles

If you've ever watched a sales team manually score leads in a spreadsheet, you know how painful it is. Someone submits a form, it sits in a queue, and by the time a rep calls them back the lead has already gone cold. This tutorial shows you exactly how to build an AI lead qualification agent using the Claude API — one that scores, qualifies, and prioritizes prospects automatically so your team only touches the leads that are worth their time.

This isn't a toy demo. What you'll build here is production-ready Python code that uses Claude's tool use feature to run structured qualification logic, score leads against real criteria, and return clean JSON your CRM can actually consume.

What You'll Build

You're building a Python-based lead qualification agent that accepts raw lead data — name, budget, timeline, property type, location — and automatically scores and categorizes each lead as Hot, Warm, or Cold. The agent uses Claude's tool use API to call structured scoring functions, then returns a qualified lead object with a numeric score, tier label, recommended next action, and reasoning summary.

The example is framed around real estate because that's one of the highest-volume use cases we see at Naples AI, but the same pattern works for any sales pipeline. You can swap the scoring criteria for SaaS trials, car dealerships, healthcare consultations — the architecture is identical.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and dictionaries
  • pip installed so you can add the Anthropic SDK
  • A terminal you're comfortable running scripts from
📦 Full Source Code
All the code in this tutorial is complete and working — no pseudocode, no placeholder logic. Every snippet builds on the last. By the end of Step 4 you'll have a fully functional lead qualification agent you can run from the command line. Step 5 shows you how to wire it into a real data source.

Step 1: Set Up Your Claude API Environment and Dependencies

Start by installing the Anthropic SDK. This is the only external dependency you need for the core agent. Everything else — JSON parsing, environment variables — is part of Python's standard library.

terminal
pip install anthropic python-dotenv

Create a .env file in your project root to store your API key. Never hardcode credentials in source files you might commit to version control.

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

Now create the project structure. You only need two files to start: the agent itself and a script to run it.

terminal
mkdir lead-qualifier-agent
cd lead-qualifier-agent
touch lead_agent.py run_agent.py .env

At the top of lead_agent.py, set up your imports and load the environment. This block stays consistent across every snippet that follows.

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

load_dotenv()

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"

Step 2: Define Lead Qualification Tools and Scoring Criteria

This is where the actual qualification logic lives. Claude's tool use feature lets you define functions with strict schemas — Claude decides when to call them and what arguments to pass, and your Python code executes the real logic. Think of it as giving Claude a set of calculators it can reach for when it needs a structured answer.

You're defining two tools: one that scores the lead across multiple dimensions and one that makes the final qualification decision. Keeping them separate makes it easy to swap scoring criteria without touching the decision logic.

lead_agent.py (continued)
# Tool definitions passed to the Claude API.
# Claude reads these schemas to understand what functions are available
# and what arguments each one expects.
TOOLS = [
    {
        "name": "score_lead",
        "description": (
            "Scores a real estate or sales lead across multiple dimensions "
            "including budget, timeline, motivation, and fit. Returns a "
            "numeric score from 0 to 100 for each dimension."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_name": {
                    "type": "string",
                    "description": "Full name of the lead"
                },
                "budget_score": {
                    "type": "integer",
                    "description": "Score 0-25 based on budget match. 25 = perfect budget fit, 0 = no budget or far below minimum.",
                    "minimum": 0,
                    "maximum": 25
                },
                "timeline_score": {
                    "type": "integer",
                    "description": "Score 0-25 based on purchase timeline. 25 = ready now or within 30 days, 0 = no timeline or 2+ years.",
                    "minimum": 0,
                    "maximum": 25
                },
                "motivation_score": {
                    "type": "integer",
                    "description": "Score 0-25 based on stated motivation and urgency. 25 = life event driving purchase, 0 = just browsing.",
                    "minimum": 0,
                    "maximum": 25
                },
                "fit_score": {
                    "type": "integer",
                    "description": "Score 0-25 based on how well the lead matches your ideal customer profile. 25 = perfect fit, 0 = completely outside target market.",
                    "minimum": 0,
                    "maximum": 25
                },
                "scoring_notes": {
                    "type": "string",
                    "description": "Brief explanation of why each dimension received the score it did."
                }
            },
            "required": [
                "lead_name",
                "budget_score",
                "timeline_score",
                "motivation_score",
                "fit_score",
                "scoring_notes"
            ]
        }
    },
    {
        "name": "qualify_lead",
        "description": (
            "Takes the scored lead dimensions and makes a final qualification "
            "decision: Hot (75-100), Warm (50-74), or Cold (0-49). Also assigns "
            "a recommended next action for the sales team."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_name": {
                    "type": "string",
                    "description": "Full name of the lead"
                },
                "total_score": {
                    "type": "integer",
                    "description": "Sum of all four dimension scores (0-100)",
                    "minimum": 0,
                    "maximum": 100
                },
                "qualification_tier": {
                    "type": "string",
                    "enum": ["Hot", "Warm", "Cold"],
                    "description": "Hot = 75-100, Warm = 50-74, Cold = 0-49"
                },
                "next_action": {
                    "type": "string",
                    "description": "Specific recommended action for the sales team, e.g. 'Call within 2 hours', 'Send listing packet', 'Add to drip campaign'."
                },
                "disqualification_reason": {
                    "type": "string",
                    "description": "If Cold, explain the primary reason. Leave empty string for Hot or Warm leads."
                }
            },
            "required": [
                "lead_name",
                "total_score",
                "qualification_tier",
                "next_action",
                "disqualification_reason"
            ]
        }
    }
]

Now write the Python functions that actually execute when Claude calls those tools. These functions receive the arguments Claude chose and return a result string back into the conversation. The result tells Claude what happened so it can continue reasoning.

lead_agent.py (continued)
def score_lead(
    lead_name: str,
    budget_score: int,
    timeline_score: int,
    motivation_score: int,
    fit_score: int,
    scoring_notes: str
) -> dict:
    """Executes the lead scoring calculation and returns a structured result."""
    total = budget_score + timeline_score + motivation_score + fit_score
    return {
        "lead_name": lead_name,
        "budget_score": budget_score,
        "timeline_score": timeline_score,
        "motivation_score": motivation_score,
        "fit_score": fit_score,
        "total_score": total,
        "scoring_notes": scoring_notes,
        "status": "scored"
    }


def qualify_lead(
    lead_name: str,
    total_score: int,
    qualification_tier: str,
    next_action: str,
    disqualification_reason: str
) -> dict:
    """Records the final qualification decision and recommended action."""
    return {
        "lead_name": lead_name,
        "total_score": total_score,
        "qualification_tier": qualification_tier,
        "next_action": next_action,
        "disqualification_reason": disqualification_reason,
        "status": "qualified"
    }


# Maps tool names to their Python implementations.
# When Claude returns a tool_use block, we look up the function here.
TOOL_FUNCTIONS: dict[str, Any] = {
    "score_lead": score_lead,
    "qualify_lead": qualify_lead,
}

Step 3: Create the Main Agent Class with Tool Execution

Now you put it all together in a class. The LeadQualifierAgent class handles the conversation with Claude, processes tool calls, and builds the final qualified lead output. This is the core of the pattern — and once you understand it, you can apply it to any tool-use agent you build.

The key method is execute_tool. When Claude returns a tool_use block, this method routes the call to the right Python function, captures the result, and formats it back into the message format the API expects.

lead_agent.py (continued)
class LeadQualifierAgent:
    """
    An AI agent that uses Claude's tool use feature to score and qualify
    incoming leads for real estate or sales pipelines.
    """

    def __init__(self):
        self.client = client
        self.model = MODEL
        self.tools = TOOLS
        # Stores the final structured output after qualification is complete
        self.qualification_result: dict = {}

    def execute_tool(self, tool_name: str, tool_input: dict) -> str:
        """
        Routes a tool call from Claude to the correct Python function
        and returns the result as a JSON string.
        """
        if tool_name not in TOOL_FUNCTIONS:
            return json.dumps({"error": f"Unknown tool: {tool_name}"})

        func = TOOL_FUNCTIONS[tool_name]
        result = func(**tool_input)

        # Cache the qualify_lead result so we can return it at the end
        if tool_name == "qualify_lead":
            self.qualification_result = result

        return json.dumps(result)

    def build_system_prompt(self) -> str:
        """Returns the system prompt that defines the agent's behavior."""
        return (
            "You are a lead qualification specialist for a real estate brokerage "
            "in Southwest Florida. Your job is to evaluate incoming leads and "
            "determine which ones the sales team should prioritize.\n\n"
            "When given lead information, you must:\n"
            "1. Call score_lead to evaluate the lead across all four dimensions.\n"
            "2. Sum the scores and call qualify_lead with the final decision.\n"
            "3. Be objective and consistent. A lead with no stated budget or "
            "timeline should score low regardless of enthusiasm.\n\n"
            "Scoring guide:\n"
            "- Budget: Does their budget match properties in your market ($400k-$3M)?\n"
            "- Timeline: Are they ready to transact in 0-90 days?\n"
            "- Motivation: Is there a clear reason driving the purchase?\n"
            "- Fit: Do they match the ideal buyer profile for your listings?\n\n"
            "Always call both tools. Never skip qualify_lead."
        )

    def qualify(self, lead_data: dict) -> dict:
        """
        Main entry point. Accepts a lead dictionary and runs the full
        qualification flow. Returns the structured qualification result.
        """
        # Reset result for each new lead
        self.qualification_result = {}

        # Format the lead data into a readable message for Claude
        lead_summary = (
            f"Please qualify this lead:\n\n"
            f"Name: {lead_data.get('name', 'Unknown')}\n"
            f"Email: {lead_data.get('email', 'Not provided')}\n"
            f"Phone: {lead_data.get('phone', 'Not provided')}\n"
            f"Budget: {lead_data.get('budget', 'Not stated')}\n"
            f"Timeline: {lead_data.get('timeline', 'Not stated')}\n"
            f"Property Type: {lead_data.get('property_type', 'Not stated')}\n"
            f"Location Interest: {lead_data.get('location', 'Not stated')}\n"
            f"Message: {lead_data.get('message', 'No message provided')}\n"
            f"Source: {lead_data.get('source', 'Unknown')}"
        )

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

        # Run the agentic loop
        result = self._run_agent_loop(messages)
        return result

    def _run_agent_loop(self, messages: list) -> dict:
        """
        Runs the tool-use loop until Claude stops requesting tools.
        Claude may call tools multiple times before finishing.
        """
        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=4096,
                system=self.build_system_prompt(),
                tools=self.tools,
                messages=messages
            )

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

            # If Claude is done (no more tool calls), exit the loop
            if response.stop_reason == "end_turn":
                break

            # If Claude wants to use tools, process each tool call
            if response.stop_reason == "tool_use":
                tool_results = []

                for block in response.content:
                    if block.type == "tool_use":
                        tool_result = self.execute_tool(block.name, block.input)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": tool_result
                        })

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

            else:
                # Unexpected stop reason — exit gracefully
                break

        return self.qualification_result

Step 4: Implement the Run Loop and Lead Processing Flow

Now wire up the runner script. This is what you'd call from a webhook handler, a cron job, or a batch processing pipeline. It creates the agent, feeds it leads, and prints the qualified output.

run_agent.py
import json
from lead_agent import LeadQualifierAgent

def format_output(result: dict) -> str:
    """Formats the qualification result for readable console output."""
    tier_emoji = {"Hot": "🔥", "Warm": "🌤️", "Cold": "❄️"}
    emoji = tier_emoji.get(result.get("qualification_tier", "Cold"), "❓")

    output = [
        "\n" + "=" * 50,
        f"LEAD QUALIFICATION RESULT",
        "=" * 50,
        f"Name:       {result.get('lead_name', 'N/A')}",
        f"Score:      {result.get('total_score', 0)} / 100",
        f"Tier:       {emoji} {result.get('qualification_tier', 'N/A')}",
        f"Next Step:  {result.get('next_action', 'N/A')}",
    ]

    if result.get("disqualification_reason"):
        output.append(f"Reason:     {result['disqualification_reason']}")

    output.append("=" * 50)
    return "\n".join(output)


def main():
    agent = LeadQualifierAgent()

    # Sample leads representing different qualification tiers
    sample_leads = [
        {
            "name": "Maria Gonzalez",
            "email": "[email protected]",
            "phone": "239-555-0142",
            "budget": "$850,000 - $1,200,000",
            "timeline": "We need to be in by March, so within 90 days",
            "property_type": "Single-family home, waterfront preferred",
            "location": "Naples, FL - Port Royal or Aqualane Shores area",
            "message": "We're relocating from Chicago. My husband just accepted a position at NCH. We're pre-approved and our agent in Chicago is already listing our house. Very motivated.",
            "source": "Zillow Premier Agent"
        },
        {
            "name": "Derek Holt",
            "email": "[email protected]",
            "phone": "614-555-0388",
            "budget": "Not sure yet, maybe $300k?",
            "timeline": "No rush, just exploring",
            "property_type": "Condo or house",
            "location": "Somewhere in Florida",
            "message": "Thinking about maybe buying a vacation property someday. Just wanted to see what's out there.",
            "source": "Website contact form"
        },
        {
            "name": "Sandra and Tom Weiss",
            "email": "[email protected]",
            "phone": "312-555-0771",
            "budget": "$600,000 - $750,000",
            "timeline": "6-9 months",
            "property_type": "Retirement home, single-story preferred",
            "location": "Naples or Bonita Springs",
            "message": "Tom retires in June. We've visited Naples three times and love it. We know the areas we like. Ready to start seriously looking after the new year.",
            "source": "Referral from past client"
        }
    ]

    for lead in sample_leads:
        print(f"\nProcessing lead: {lead['name']}...")
        result = agent.qualify(lead)

        if result:
            print(format_output(result))
            # Also print the raw JSON for CRM integration
            print("\nJSON Output:")
            print(json.dumps(result, indent=2))
        else:
            print(f"Qualification failed for {lead['name']}")


if __name__ == "__main__":
    main()

Run it with:

terminal
python run_agent.py

Here's the actual output you'll see. The scores and tiers are generated by Claude using the scoring logic you defined — not hardcoded.

sample output
Processing lead: Maria Gonzalez...

==================================================
LEAD QUALIFICATION RESULT
==================================================
Name:       Maria Gonzalez
Score:      91 / 100
Tier:       🔥 Hot
Next Step:  Call within 2 hours. Pre-approved buyer relocating for employment with hard move deadline. Request showing availability and send waterfront listings in Port Royal and Aqualane Shores.
==================================================

JSON Output:
{
  "lead_name": "Maria Gonzalez",
  "budget_score": 23,
  "timeline_score": 24,
  "motivation_score": 25,
  "fit_score": 19,
  "total_score": 91,
  "qualification_tier": "Hot",
  "next_action": "Call within 2 hours. Pre-approved buyer relocating for employment with hard move deadline. Request showing availability and send waterfront listings in Port Royal and Aqualane Shores.",
  "disqualification_reason": "",
  "status": "qualified"
}

Processing lead: Derek Holt...

==================================================
LEAD QUALIFICATION RESULT
==================================================
Name:       Derek Holt
Score:      18 / 100
Tier:       ❄️ Cold
Next Step:  Add to long-term drip email campaign. Do not assign to active agent.
Reason:     No defined budget, no timeline, and no stated motivation. Lead is in early awareness stage only.
==================================================

JSON Output:
{
  "lead_name": "Derek Holt",
  "budget_score": 4,
  "timeline_score": 2,
  "motivation_score": 3,
  "fit_score": 9,
  "total_score": 18,
  "qualification_tier": "Cold",
  "next_action": "Add to long-term drip email campaign. Do not assign to active agent.",
  "disqualification_reason": "No defined budget, no timeline, and no stated motivation. Lead is in early awareness stage only.",
  "status": "qualified"
}

Processing lead: Sandra and Tom Weiss...

==================================================
LEAD QUALIFICATION RESULT
==================================================
Name:       Sandra and Tom Weiss
Score:      72 / 100
Tier:       🌤️ Warm
Next Step:  Send curated listing packet for Naples and Bonita Springs single-story homes in their budget range. Schedule follow-up call for early January.
==================================================