What You'll Build
If you're manually reviewing leads every day, you already know how much time it eats up. In this tutorial, you'll build a production-ready AI lead qualifier agent in Python using the Claude API that scores incoming leads, asks clarifying questions, and outputs a qualification decision — automatically.
By the end, you'll have a working agentic loop that can process 50+ leads daily without anyone touching a keyboard. The agent uses Claude's tool use feature to call custom scoring functions and simulate CRM writes, so it's extendable into a real pipeline.
The complete working code for this lead qualifier agent is broken into steps below. Each step builds on the last — by Step 5, you'll have a single script you can run end-to-end. Copy the snippets in order and you'll have everything you need.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
anthropicPython SDK installed:pip install anthropic- Basic comfort with Python classes and functions
- Optional: a free CRM account (HubSpot, Airtable) if you want to wire up real CRM writes later
Step 1: Set Up Your Claude API Client and Define Agent Tools
First, let's set up the Anthropic client and define the tools our agent will have access to. Think of tools as functions the AI can decide to call — it reads the lead data and then chooses which tool to use based on what it needs to do next.
We're going to define two tools: score_lead and save_to_crm. The agent will call these in sequence as it qualifies each lead.
import anthropic
import os
# Initialize the Anthropic client using your API key from environment
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
# Tool definitions tell Claude what functions are available and what parameters they expect
TOOLS = [
{
"name": "score_lead",
"description": (
"Scores an incoming lead based on budget, timeline, company size, "
"and intent signals. Returns a score from 0-100 and a qualification tier."
),
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Full name of the lead"},
"company": {"type": "string", "description": "Company name"},
"budget": {
"type": "string",
"description": "Stated budget range, e.g. '$5,000-$10,000'"
},
"timeline": {
"type": "string",
"description": "How soon they want to start, e.g. 'within 30 days'"
},
"company_size": {
"type": "integer",
"description": "Number of employees at the company"
},
"pain_point": {
"type": "string",
"description": "The main business problem they described"
},
},
"required": ["name", "company", "budget", "timeline", "company_size", "pain_point"],
},
},
{
"name": "save_to_crm",
"description": (
"Saves the qualified lead and their score to the CRM system. "
"Should only be called after score_lead has been run."
),
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Full name of the lead"},
"company": {"type": "string", "description": "Company name"},
"score": {"type": "integer", "description": "Lead score from 0-100"},
"tier": {
"type": "string",
"description": "Qualification tier: Hot, Warm, or Cold"
},
"recommended_action": {
"type": "string",
"description": "Next step recommended for this lead"
},
},
"required": ["name", "company", "score", "tier", "recommended_action"],
},
},
]
Notice we're not hardcoding the API key — it pulls from your environment variable. Run export ANTHROPIC_API_KEY=your_key_here in your terminal before executing the script.
Step 2: Create the Lead Qualifier Agent Class
Now let's build the agent class that ties everything together. This class holds the conversation history, sends messages to Claude, and manages the overall qualification flow.
The key idea here is that Claude drives the logic. You give it the lead data and it decides which tools to call and in what order — you're not hardcoding a decision tree.
lead_qualifier_agent.py
import anthropic
import os
import json
from lead_qualifier_tools import client, MODEL, TOOLS
class LeadQualifierAgent:
"""
An AI agent that qualifies inbound leads using Claude's tool use feature.
The agent scores leads, categorizes them, and logs results to a CRM.
"""
def __init__(self):
self.conversation_history = []
self.system_prompt = """You are an expert sales development representative AI for a
Southwest Florida AI agency called Naples AI. Your job is to qualify inbound leads by
analyzing their budget, timeline, company size, and pain points.
Always follow this sequence:
1. First call score_lead with the lead's information to generate a score and tier.
2. Then call save_to_crm with the score results and a recommended next action.
3. Finally, provide a brief natural-language summary of your qualification decision.
Be direct and decisive. Do not ask for more information — work with what you have.
A score of 70-100 is Hot, 40-69 is Warm, and 0-39 is Cold."""
def qualify_lead(self, lead_data: dict) -> dict:
"""
Main entry point. Takes a lead dictionary and runs it through the full
qualification pipeline. Returns the final qualification result.
"""
# Format the lead data into a natural message for Claude
user_message = f"""Please qualify this incoming lead:
Name: {lead_data.get('name')}
Company: {lead_data.get('company')}
Email: {lead_data.get('email')}
Budget: {lead_data.get('budget')}
Timeline: {lead_data.get('timeline')}
Company Size: {lead_data.get('company_size')} employees
Pain Point: {lead_data.get('pain_point')}
Run them through our qualification process and save the result to our CRM."""
self.conversation_history.append({
"role": "user",
"content": user_message
})
# Kick off the agentic loop and return the final result
result = self._run_agent_loop()
return result
def _run_agent_loop(self) -> dict:
"""
The agentic loop. Keeps calling Claude until it stops requesting tool calls.
This is the core of how tool-use agents work with the Anthropic SDK.
"""
tool_results_collected = []
final_text = ""
iteration = 0
max_iterations = 10 # Safety cap to prevent infinite loops
while iteration < max_iterations:
iteration += 1
response = client.messages.create(
model=MODEL,
max_tokens=4096,
system=self.system_prompt,
tools=TOOLS,
messages=self.conversation_history,
)
# Collect any text content from this response turn
for block in response.content:
if hasattr(block, "text"):
final_text = block.text
# If Claude is done using tools, break out of the loop
if response.stop_reason == "end_turn":
break
# If Claude wants to use tools, process them all before the next turn
if response.stop_reason == "tool_use":
# Add Claude's full response (including tool calls) to history
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
tool_results = self._process_tool_calls(response.content)
tool_results_collected.extend(tool_results)
# Feed all tool results back to Claude as a single user turn
self.conversation_history.append({
"role": "user",
"content": tool_results
})
return {
"summary": final_text,
"tool_results": tool_results_collected,
}
Step 3: Implement the Lead Scoring and CRM Tool Logic
Claude decides when to call a tool, but your Python code actually runs it. This step defines what happens when score_lead and save_to_crm get called.
The scoring function uses a simple weighted algorithm — you can swap this out for a machine learning model later if you want. The CRM function just prints for now, but I'll show you how to wire it to a real API in the Next Steps section.
tool_implementations.py
import json
from datetime import datetime
def execute_score_lead(params: dict) -> dict:
"""
Scores a lead from 0-100 based on budget, timeline, and company size.
Returns a score, tier, and recommended action.
"""
score = 0
# --- Budget scoring (max 40 points) ---
budget_str = params.get("budget", "").lower()
if any(x in budget_str for x in ["50,000", "100,000", "500,000", "50k", "100k"]):
score += 40
elif any(x in budget_str for x in ["10,000", "25,000", "10k", "25k"]):
score += 25
elif any(x in budget_str for x in ["5,000", "5k"]):
score += 10
else:
score += 5 # Unknown budget still gets a few points
# --- Timeline scoring (max 30 points) ---
timeline_str = params.get("timeline", "").lower()
if any(x in timeline_str for x in ["immediately", "asap", "this week", "this month"]):
score += 30
elif any(x in timeline_str for x in ["30 days", "next month", "q1", "q2", "q3", "q4"]):
score += 20
elif "6 months" in timeline_str or "next year" in timeline_str:
score += 10
else:
score += 5
# --- Company size scoring (max 30 points) ---
company_size = params.get("company_size", 0)
if company_size >= 200:
score += 30
elif company_size >= 50:
score += 20
elif company_size >= 10:
score += 15
else:
score += 5
# Determine tier based on total score
if score >= 70:
tier = "Hot"
action = "Schedule a discovery call within 24 hours"
elif score >= 40:
tier = "Warm"
action = "Send a personalized follow-up email and add to nurture sequence"
else:
tier = "Cold"
action = "Add to general newsletter list and follow up in 90 days"
return {
"name": params["name"],
"company": params["company"],
"score": score,
"tier": tier,
"recommended_action": action,
"scored_at": datetime.utcnow().isoformat(),
}
def execute_save_to_crm(params: dict) -> dict:
"""
Saves a qualified lead to the CRM. In production, replace the print
statements with an API call to HubSpot, Airtable, or your CRM of choice.
"""
print(f"\n[CRM WRITE] Saving lead: {params['name']} @ {params['company']}")
print(f" Score: {params['score']} | Tier: {params['tier']}")
print(f" Action: {params['recommended_action']}")
# Simulated CRM response — replace with requests.post() to your real CRM endpoint
crm_record = {
"status": "success",
"crm_id": f"LEAD-{abs(hash(params['name'])) % 100000:05d}",
"saved_fields": list(params.keys()),
}
return crm_record
Step 4: Build the Agentic Loop with Tool Use and Iteration
This is the piece that makes the agent actually work. We need to process the tool calls Claude requests, execute the right Python function, and return the results in the format the SDK expects.
Add the _process_tool_calls method to your LeadQualifierAgent class. This method reads each tool call block from Claude's response and dispatches it to the right function.
# Add this method inside the LeadQualifierAgent class from Step 2
# This replaces the _process_tool_calls reference in _run_agent_loop
import json
from tool_implementations import execute_score_lead, execute_save_to_crm
def _process_tool_calls(self, response_content: list) -> list:
"""
Iterates over Claude's response blocks and executes any tool_use blocks.
Returns a list of tool_result blocks formatted for the next API call.
"""
tool_results = []
for block in response_content:
# Only process tool_use blocks — skip text blocks
if block.type != "tool_use":
continue
tool_name = block.name
tool_input = block.input
print(f"\n[AGENT] Calling tool: {tool_name}")
print(f"[AGENT] With inputs: {json.dumps(tool_input, indent=2)}")
# Dispatch to the correct implementation function
if tool_name == "score_lead":
result = execute_score_lead(tool_input)
elif tool_name == "save_to_crm":
result = execute_save_to_crm(tool_input)
else:
result = {"error": f"Unknown tool: {tool_name}"}
print(f"[AGENT] Tool result: {json.dumps(result, indent=2)}")
# Format the result exactly as the Anthropic SDK expects
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # Must match the tool_use block's id
"content": json.dumps(result),
})
return tool_results
tool_use_id in your tool result must exactly match the id field from Claude's tool_use block. If they don't match, the API will throw a validation error. The code above handles this automatically via block.id.
Step 5: Test with Sample Lead Data
Now let's put it all together and run a real test. Here's the complete runner script that creates the agent, feeds it a sample lead, and prints the output.
I've included two test leads — one that should score Hot and one that should score Cold — so you can see both paths working.
run_qualifier.py
import anthropic
import os
import json
from datetime import datetime
# ── Tool definitions ──────────────────────────────────────────────────────────
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"
TOOLS = [
{
"name": "score_lead",
"description": (
"Scores an incoming lead based on budget, timeline, company size, "
"and intent signals. Returns a score from 0-100 and a qualification tier."
),
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"company": {"type": "string"},
"budget": {"type": "string"},
"timeline": {"type": "string"},
"company_size": {"type": "integer"},
"pain_point": {"type": "string"},
},
"required": ["name", "company", "budget", "timeline", "company_size", "pain_point"],
},
},
{
"name": "save_to_crm",
"description": "Saves the qualified lead and score to the CRM. Call after score_lead.",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"company": {"type": "string"},
"score": {"type": "integer"},
"tier": {"type": "string"},
"recommended_action": {"type": "string"},
},
"required": ["name", "company", "score", "tier", "recommended_action"],
},
},
]
# ── Tool implementations ──────────────────────────────────────────────────────
def execute_score_lead(params: dict) -> dict:
score = 0
budget_str = params.get("budget", "").lower()
if any(x in budget_str for x in ["50,000", "100,000", "50k", "100k"]):
score += 40
elif any(x in budget_str for x in ["10,000", "25,000", "10k", "25k"]):
score += 25
elif any(x in budget_str for x in ["5,000", "5k"]):
score += 10
else:
score += 5
timeline_str = params.get("timeline", "").lower()
if any(x in timeline_str for x in ["immediately", "asap", "this week", "this month"]):
score += 30
elif any(x in timeline_str for x in ["30 days", "next month"]):
score += 20
elif "6 months" in timeline_str:
score += 10
else:
score += 5
company_size = params.get("company_size", 0)
if company_size >= 200:
score += 30
elif company_size >= 50:
score += 20
elif company_size >= 10:
score += 15
else:
score += 5
if score >= 70:
tier = "Hot"
action = "Schedule a discovery call within 24 hours"
elif score >= 40:
tier = "Warm"
action = "Send a personalized follow-up email and add to nurture sequence"
else:
tier = "Cold"
action = "Add to general newsletter list and follow up in 90 days"
return {
"name": params["name"],
"company": params["company"],
"score": score,
"tier": tier,
"recommended_action": action,
"scored_at": datetime.utcnow().isoformat(),
}
def execute_save_to_crm(params: dict) -> dict:
print(f"\n [CRM] Saving: {params['name']} @ {params['company']}")
print(f" [CRM] Score: {params['score']} | Tier: {params['tier']}")
return {
"status": "success",
"crm_id": f"LEAD-{abs(hash(params['name'])) % 100000:05d}",
}
def process_tool_calls(response_content: list) -> list:
tool_results = []
for block in response_content:
if block.type != "tool_use":
continue
print(f"\n [TOOL CALL] {block.name}({json.dumps(block.input, indent=4)})")
if block.name == "score_lead":
result = execute_score_lead(block.input)
elif block.name == "save_to_crm":
result = execute_save_to_crm(block.input)
else:
result = {"error": f"Unknown tool: {block.name}"}
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
return tool_results
# ── Agent class ───────────────────────────────────────────────────────────────
class LeadQualifierAgent:
def __init__(self):
self.conversation_history = []
self.system_prompt = """You are an expert sales development representative AI for
Naples AI, a Southwest Florida AI agency. Qualify inbound leads using the available tools.
Always follow this sequence:
1. Call score_lead with the lead's information.
2. Call save_to_crm with the score results and recommended action.
3. Provide a concise natural-language summary of the qualification decision.
Score tiers: Hot = 70-100, Warm = 40-69, Cold = 0-39. Be direct and decisive."""
def qualify_lead(self, lead_data: dict) -> dict:
user_message = f"""Please qualify this incoming lead:
Name: {lead_data.get('name')}
Company: {lead_data.get('company')}
Email: {lead_data.get('email')}
Budget: {lead_data.get('budget')}
Timeline: {lead_data.get('timeline')}
Company Size: {lead_data.get('company_size')} employees
Pain Point: {lead_data.get('pain_point')}"""