If you're tired of manually sorting through leads and trying to figure out which prospects are actually worth your time, you're in the right place. In this tutorial, I'll show you exactly how to build an AI lead qualifier agent using the Claude API that automatically scores and qualifies incoming leads — in under 150 lines of Python.
This is the same type of automation we build at Naples AI for clients in real estate, healthcare, and car dealerships. It's practical, production-ready, and you can have it running today.
What You'll Build
You'll build a working AI agent in Python that takes raw lead data — name, company, budget, pain points, timeline — and returns a structured qualification score with a recommendation. The agent uses Claude's tool use (function calling) feature to run a multi-step reasoning loop before producing a final JSON output.
By the end, you'll have a reusable LeadQualifierAgent class you can drop into any CRM pipeline, webhook handler, or internal tool. No fluff — just working code.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one here)
anthropicPython SDK installed:pip install anthropic- Basic familiarity with Python classes and dictionaries
- A terminal or IDE you're comfortable running Python scripts in
Step 1: Initialize the Claude Client and Define the Lead Schema
First, let's set up the Anthropic client and define what a lead looks like in our system. I like using a TypedDict here because it makes the data contract explicit — you'll thank yourself later when you're piping in leads from a form or CRM webhook.
lead_qualifier.pyimport os
import json
from typing import TypedDict, Optional
import anthropic
# Initialize the Anthropic client using ANTHROPIC_API_KEY from environment
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
class Lead(TypedDict):
name: str
company: str
email: str
budget_usd: int
pain_point: str
timeline_days: int
industry: str
employee_count: Optional[int]
# Example lead we'll use throughout this tutorial
SAMPLE_LEAD: Lead = {
"name": "Maria Gonzalez",
"company": "Gulf Coast Realty Group",
"email": "[email protected]",
"budget_usd": 18000,
"pain_point": "Manually entering new listings into our website takes 3 hours per agent per week",
"timeline_days": 30,
"industry": "real_estate",
"employee_count": 22,
}Nothing fancy here — just a clean schema and a realistic sample lead. The SAMPLE_LEAD is based on the exact type of prospect we see at Naples AI: a real estate company drowning in manual data entry that wants to automate their listing workflow.
Step 2: Create Tool Definitions for Lead Qualification
This is where it gets interesting. Claude's tool use feature lets the model call functions during its reasoning process. We're going to define three tools: one to score budget fit, one to assess urgency, and one to submit the final qualification decision.
Think of these tools as the checklist your best salesperson runs through mentally before deciding whether to pursue a lead. We're just making that process explicit and automatable.
lead_qualifier.py (continued)LEAD_QUALIFICATION_TOOLS = [
{
"name": "score_budget_fit",
"description": (
"Evaluates whether the lead's stated budget aligns with realistic project costs. "
"Returns a score from 0-100 and a short rationale."
),
"input_schema": {
"type": "object",
"properties": {
"budget_usd": {
"type": "integer",
"description": "The lead's stated budget in US dollars"
},
"industry": {
"type": "string",
"description": "The lead's industry vertical"
},
"pain_point": {
"type": "string",
"description": "The problem the lead is trying to solve"
}
},
"required": ["budget_usd", "industry", "pain_point"]
}
},
{
"name": "score_urgency",
"description": (
"Evaluates lead urgency based on stated timeline and pain point severity. "
"Returns a score from 0-100 and a short rationale."
),
"input_schema": {
"type": "object",
"properties": {
"timeline_days": {
"type": "integer",
"description": "How many days until the lead needs a solution"
},
"pain_point": {
"type": "string",
"description": "Description of the business problem"
}
},
"required": ["timeline_days", "pain_point"]
}
},
{
"name": "submit_qualification_result",
"description": (
"Submits the final lead qualification result after all scoring is complete. "
"Call this only after running the other scoring tools."
),
"input_schema": {
"type": "object",
"properties": {
"overall_score": {
"type": "integer",
"description": "Overall lead quality score from 0-100"
},
"qualification_tier": {
"type": "string",
"enum": ["hot", "warm", "cold", "disqualified"],
"description": "Lead tier based on overall score"
},
"recommended_action": {
"type": "string",
"description": "Specific next step the sales team should take"
},
"budget_score": {
"type": "integer",
"description": "Budget fit score from 0-100"
},
"urgency_score": {
"type": "integer",
"description": "Urgency score from 0-100"
},
"summary": {
"type": "string",
"description": "Two-sentence summary of why this lead was scored this way"
}
},
"required": [
"overall_score",
"qualification_tier",
"recommended_action",
"budget_score",
"urgency_score",
"summary"
]
}
}
]Step 3: Build the Agent Loop with Tool Use
Here's the core of the agent — the agentic loop. Claude sends back a tool call, we execute the tool, send the result back, and keep going until Claude submits the final qualification result. This is how every real Claude API multi-agent system works under the hood.
I want to be clear: the tool "execution" here means we're processing the tool call inputs and returning results. For score_budget_fit and score_urgency, we let Claude do the reasoning and just echo the call back. The submit_qualification_result tool is what we actually intercept to capture the final output.
def process_tool_call(tool_name: str, tool_input: dict) -> str:
"""
Handles tool execution during the agent loop.
For scoring tools, we return an acknowledgment so Claude continues reasoning.
For the final submission tool, we signal completion.
"""
if tool_name == "score_budget_fit":
return json.dumps({
"status": "scored",
"tool": "score_budget_fit",
"input_received": tool_input,
"note": "Score recorded. Proceed to urgency scoring."
})
elif tool_name == "score_urgency":
return json.dumps({
"status": "scored",
"tool": "score_urgency",
"input_received": tool_input,
"note": "Score recorded. Proceed to final qualification submission."
})
elif tool_name == "submit_qualification_result":
# This is the tool we actually care about — the final result
return json.dumps({
"status": "qualification_complete",
"result_captured": True
})
return json.dumps({"status": "unknown_tool"})
class LeadQualifierAgent:
def __init__(self, client: anthropic.Anthropic, model: str):
self.client = client
self.model = model
def qualify(self, lead: Lead) -> dict:
"""
Runs the full agent loop for a single lead.
Returns the final qualification result as a dictionary.
"""
system_prompt = (
"You are an expert B2B sales qualification agent. "
"Your job is to evaluate leads for an AI solutions agency in Southwest Florida. "
"Use the provided tools in order: first score budget fit, then score urgency, "
"then submit the final qualification result. "
"Be direct and specific in your reasoning. "
"A budget under $5,000 is almost always disqualified for custom AI work. "
"A timeline under 14 days signals high urgency."
)
# Format the lead data into a human-readable prompt
lead_prompt = (
f"Please qualify this lead:\n\n"
f"Name: {lead['name']}\n"
f"Company: {lead['company']}\n"
f"Industry: {lead['industry']}\n"
f"Budget: ${lead['budget_usd']:,}\n"
f"Pain Point: {lead['pain_point']}\n"
f"Timeline: {lead['timeline_days']} days\n"
f"Team Size: {lead.get('employee_count', 'Unknown')} employees\n"
)
messages = [{"role": "user", "content": lead_prompt}]
final_result = None
# Agent loop — runs until Claude calls submit_qualification_result
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=system_prompt,
tools=LEAD_QUALIFICATION_TOOLS,
messages=messages
)
# Add Claude's response to the message history
messages.append({"role": "assistant", "content": response.content})
# Check if Claude is done (no more tool calls)
if response.stop_reason == "end_turn":
break
# Process any tool calls Claude made in this turn
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_result = process_tool_call(block.name, block.input)
# Capture the final result before it gets swallowed by the loop
if block.name == "submit_qualification_result":
final_result = block.input
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": tool_result
})
# If we captured the final result, we're done
if final_result is not None:
break
# Send tool results back to Claude if there are any
if tool_results:
messages.append({"role": "user", "content": tool_results})
return final_result or {}Step 4: Implement the Lead Scoring Run Loop
Now let's wire everything together with a main execution block. This is the entry point you'd call from a webhook handler, a scheduled job, or a batch processing script. I've also added a simple batch processor so you can run a list of leads in one shot.
lead_qualifier.py (continued)def run_lead_qualification(lead: Lead) -> dict:
"""
Entry point for qualifying a single lead.
Returns the full qualification result dictionary.
"""
agent = LeadQualifierAgent(client=client, model=MODEL)
print(f"\n🔍 Qualifying lead: {lead['name']} from {lead['company']}...")
result = agent.qualify(lead)
return result
def batch_qualify_leads(leads: list[Lead]) -> list[dict]:
"""
Runs qualification on a list of leads and returns all results.
Useful for processing CRM exports or webhook batches.
"""
results = []
for lead in leads:
result = run_lead_qualification(lead)
result["lead_email"] = lead["email"] # Attach email for easy lookup
result["lead_name"] = lead["name"]
results.append(result)
return results
if __name__ == "__main__":
# Run qualification on our sample lead
result = run_lead_qualification(SAMPLE_LEAD)
print("\n✅ Qualification Complete")
print("=" * 50)
print(json.dumps(result, indent=2))When you run this script, you'll see the agent work through the tool calls in real time, then print the final structured result. Here's what the actual output looks like:
example_output.json{
"overall_score": 82,
"qualification_tier": "hot",
"recommended_action": "Schedule a 30-minute discovery call within 48 hours. Lead has clear ROI potential — 3 hours/agent/week at 22 agents is substantial. Prepare a real estate listing automation case study.",
"budget_score": 78,
"urgency_score": 85,
"summary": "Gulf Coast Realty Group has a well-defined, quantifiable pain point with a 30-day timeline and a realistic budget for a custom listing automation solution. The combination of high urgency, clear business impact, and a budget above the minimum threshold makes this a strong candidate for immediate outreach.",
"lead_email": "[email protected]",
"lead_name": "Maria Gonzalez"
}How It Works
The agent follows a clear three-step reasoning pattern enforced by the tool structure. Claude starts by calling score_budget_fit, which forces it to think about whether the money is there. Then it calls score_urgency to reason about timeline and pain intensity. Finally, it synthesizes both scores into a single submit_qualification_result call.
The loop in LeadQualifierAgent.qualify() keeps the conversation going by feeding each tool result back into the message history. This is the standard pattern for Claude API multi-agent tool use — Claude sees the tool results and decides what to do next.
We capture the final result by intercepting the submit_qualification_result tool call inputs before returning them. The agent breaks out of the loop as soon as that tool is called, so we never wait for unnecessary extra turns.
Common Errors and Fixes
AuthenticationError: Error code: 401What happened: Your API key isn't set or isn't being read correctly.
Fix: Run
export ANTHROPIC_API_KEY="sk-ant-..." in your terminal before running the script, or add it to a .env file and load it with python-dotenv. Never hardcode the key directly in the file.
anthropic.BadRequestError: messages: roles must alternate between "user" and "assistant"What happened: You added two user messages or two assistant messages in a row to the message history.
Fix: Make sure you append Claude's full response (as an assistant message) before appending your tool results (as a user message). The order in the agent loop matters — assistant turn, then user turn with tool results, then repeat.
KeyError: 'type' or AttributeError: 'str' object has no attribute 'type'What happened: You're iterating over
response.content and treating all blocks as tool use blocks, but Claude sometimes returns plain text blocks too.Fix: Always check
if block.type == "tool_use" before accessing block.name or block.input. The loop in the code above already handles this correctly — make sure you didn't accidentally remove that conditional.
Next Steps
Now that you have a working lead qualifier, here are a few ways to take it further:
- Connect it to a form webhook. Point a Typeform, Tally, or GoHighLevel form submission webhook at a Flask or FastAPI endpoint that calls
run_lead_qualification()automatically when a new lead comes in. - Add an industry-specific scoring tool. Create a fourth tool called
score_industry_fitthat checks whether the lead's industry matches your agency's highest-margin verticals. For Naples AI, that would weight real estate and healthcare leads higher. - Log results to a database. Pipe the output JSON into a Supabase or PostgreSQL table. After 100 leads, you'll have enough data to start spotting patterns in which lead attributes actually predict a closed deal.
- Add a follow-up email drafter. After qualification, pass the result to a second Claude call that drafts a personalized outreach email based on the lead's pain point and tier. Hot leads get a detailed proposal request; warm leads get a case study.
FAQ
How do I add more tools to a Claude API agent?
Just add another dictionary to your LEAD_QUALIFICATION_TOOLS list following the same schema structure. Each tool needs a name, a description (this is what Claude reads to decide when to call it), and an input_schema that matches JSON Schema format. Then add a handler case for it in your process_tool_call() function.
What is the difference between Claude tool use and function calling?
They're the same concept with different names. OpenAI calls it "function calling," Anthropic calls it "tool use." Both let the model decide when to call an external function during a conversation, passing structured inputs that your code then executes. The implementation syntax differs, but the underlying pattern — model requests a tool call, you run it, you return the result — is identical.
How do I run a Claude agent in a loop without it running forever?
Two safeguards work well together. First, define a terminal tool like submit_qualification_result that signals the agent is done — break out of the loop when that tool is called. Second, add a maximum iteration counter (e.g., max_turns = 10) and raise an error or return a partial result if the loop hits that limit. This prevents runaway API calls from a model that gets confused.
Can I use this lead scoring automation with GPT-4 instead of Claude?
Yes, the pattern translates directly. The main difference is the API call structure: OpenAI uses tools with a function type and checks finish_reason == "tool_calls", while Anthropic uses tools directly and checks stop_reason == "tool_use". The loop logic and tool definition pattern are nearly identical. That said, in our testing at Naples AI, Claude tends to follow multi-step tool sequences more reliably without needing extra prompt reinforcement.
How much does it cost to run this agent per lead?
With claude-sonnet-4-5, a typical qualification run uses roughly 800–1,200 input tokens and 300–500 output tokens per lead. At current Anthropic pricing, that's well under $0.01 per lead. For a business processing 500 leads a month,