If you're losing leads because your team can't follow up fast enough, you're not alone. Most small and mid-size businesses in Southwest Florida have the same problem — leads come in, get stuck in a spreadsheet, and go cold before anyone scores or contacts them. This tutorial shows you exactly how to build an AI lead qualifier agent using the Claude API that automates lead scoring and follow-up in under 200 lines of Python.
We built a version of this for real estate clients at Naples AI, and it cut their lead response time from hours to seconds. By the end of this guide, you'll have a working agent you can plug into any lead pipeline.
What You'll Build
You'll build a Python agent that takes raw lead data — name, email, budget, timeline, and source — and uses Claude to extract structured information, score the lead from 0 to 100, and output a prioritized follow-up recommendation. The agent uses Claude's tool-use feature to keep extractions consistent and reliable.
The whole thing runs from the command line and is ready to wire into a CRM webhook, a form submission endpoint, or a batch processing script. It's production-ready from day one.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python functions and dictionaries
- The
anthropicPython SDK (pip install anthropic) - The
python-dotenvpackage for managing your API key (pip install python-dotenv)
Step 1: Set Up Your Claude API Key and Python Environment
First, create a project folder and a .env file to store your API key. Never hardcode credentials — this keeps your key out of version control and makes deployment cleaner.
ANTHROPIC_API_KEY=your_api_key_here
Now install your dependencies if you haven't already.
terminalpip install anthropic python-dotenv
Create your main file and set up the base imports and client. This is the foundation everything else builds on.
lead_qualifier.pyimport os
import json
from dotenv import load_dotenv
import anthropic
# Load API key from .env file
load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
That's all the setup you need. The Anthropic SDK handles the rest of the HTTP connection automatically.
Step 2: Define Tool Schemas for Lead Data Extraction
Claude's tool-use feature lets you define structured outputs — instead of getting a blob of text back, you get clean JSON every time. This is what makes the agent reliable enough to use in production.
We're defining two tools: one to extract lead information from unstructured input, and one to return the final score and recommendation. Add this to your file right after the client setup.
lead_qualifier.pyTOOLS = [
{
"name": "extract_lead_data",
"description": (
"Extract structured lead information from raw input text. "
"Use this tool to parse contact details, intent signals, "
"budget, and timeline from any lead message or form submission."
),
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Full name of the lead"
},
"email": {
"type": "string",
"description": "Email address of the lead"
},
"phone": {
"type": "string",
"description": "Phone number if provided, otherwise empty string"
},
"budget": {
"type": "number",
"description": "Estimated budget in USD. Use 0 if not mentioned."
},
"timeline": {
"type": "string",
"description": "Buying or project timeline (e.g. 'within 30 days', 'Q1 2027', 'not specified')"
},
"intent_signals": {
"type": "array",
"items": {"type": "string"},
"description": "List of phrases or signals that indicate buying intent"
},
"source": {
"type": "string",
"description": "Where the lead came from (e.g. 'website form', 'referral', 'paid ad')"
},
"notes": {
"type": "string",
"description": "Any other relevant details about the lead's needs"
}
},
"required": ["name", "email", "budget", "timeline", "intent_signals", "source"]
}
},
{
"name": "score_and_recommend",
"description": (
"Given extracted lead data, return a numeric score from 0-100 "
"and a follow-up recommendation. Call this after extract_lead_data."
),
"input_schema": {
"type": "object",
"properties": {
"score": {
"type": "integer",
"description": "Lead quality score from 0 (cold) to 100 (hot)"
},
"priority": {
"type": "string",
"enum": ["hot", "warm", "cold"],
"description": "Priority tier based on score"
},
"follow_up_action": {
"type": "string",
"description": "Specific recommended next action for the sales team"
},
"follow_up_timing": {
"type": "string",
"description": "When to follow up (e.g. 'within 1 hour', 'within 24 hours', 'within 1 week')"
}
},
"required": ["score", "priority", "follow_up_action", "follow_up_timing"]
}
}
]
The input_schema block is just a JSON Schema definition. Claude reads it and knows exactly what fields to populate. Think of it like a typed function signature that Claude has to respect.
Step 3: Build the Main Agent Loop with Message Handling
This is the core of the agent. The loop sends a message to Claude, handles any tool calls it makes, feeds the results back, and keeps going until Claude returns a final answer. This agentic loop pattern works for almost any multi-step task.
lead_qualifier.pyclass LeadQualifierAgent:
def __init__(self):
self.model = "claude-sonnet-4-6"
self.extracted_data = {}
self.score_data = {}
def process_tool_call(self, tool_name: str, tool_input: dict) -> str:
"""Route tool calls to the appropriate handler and return a JSON string result."""
if tool_name == "extract_lead_data":
self.extracted_data = tool_input
return json.dumps({"status": "extracted", "data": tool_input})
elif tool_name == "score_and_recommend":
# Merge scoring data with previously extracted lead data
self.score_data = tool_input
return json.dumps({"status": "scored", "data": tool_input})
return json.dumps({"error": f"Unknown tool: {tool_name}"})
def qualify_lead(self, raw_lead_text: str) -> dict:
"""
Run the full qualification loop for a single lead.
Returns a dict with extracted data and score.
"""
system_prompt = (
"You are a lead qualification specialist. When given raw lead information, "
"you MUST call extract_lead_data first to parse the details, then call "
"score_and_recommend to evaluate the lead. Always use both tools in order. "
"Score leads higher when they have a clear budget, short timeline, and strong intent signals."
)
messages = [
{"role": "user", "content": f"Qualify this lead:\n\n{raw_lead_text}"}
]
# Agentic loop — keeps running until Claude stops calling tools
while True:
response = client.messages.create(
model=self.model,
max_tokens=1024,
system=system_prompt,
tools=TOOLS,
messages=messages
)
# Collect all tool uses from this response
tool_uses = [block for block in response.content if block.type == "tool_use"]
if not tool_uses:
# Claude is done calling tools — exit the loop
break
# Build the assistant message with the full response content
messages.append({"role": "assistant", "content": response.content})
# Process each tool call and build the tool result message
tool_results = []
for tool_use in tool_uses:
result = self.process_tool_call(tool_use.name, tool_use.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result
})
# Feed results back to Claude so it can continue
messages.append({"role": "user", "content": tool_results})
# Stop if Claude has finished its turn (end_turn or no more tool calls expected)
if response.stop_reason == "end_turn":
break
# Merge both tool outputs into a single result dict
return {**self.extracted_data, **self.score_data}
The key thing to understand here: every time Claude calls a tool, you have to send the result back before it can continue. The loop handles that automatically. If you skip the feedback step, Claude will stall or repeat itself.
Step 4: Implement Lead Scoring Logic
Claude handles the AI-side scoring through the score_and_recommend tool, but you want a local validation layer too. This function checks the score Claude returned and formats the final output your team actually sees.
def format_lead_report(lead_result: dict) -> str:
"""
Take the raw agent output and format it into a readable report.
This is what gets sent to your CRM, Slack, or email system.
"""
score = lead_result.get("score", 0)
priority = lead_result.get("priority", "cold")
name = lead_result.get("name", "Unknown")
email = lead_result.get("email", "N/A")
phone = lead_result.get("phone", "N/A")
budget = lead_result.get("budget", 0)
timeline = lead_result.get("timeline", "Not specified")
intent_signals = lead_result.get("intent_signals", [])
follow_up_action = lead_result.get("follow_up_action", "Review manually")
follow_up_timing = lead_result.get("follow_up_timing", "Within 48 hours")
source = lead_result.get("source", "Unknown")
notes = lead_result.get("notes", "")
# Priority emoji for quick visual scanning in reports
priority_icons = {"hot": "🔥", "warm": "🟡", "cold": "🔵"}
icon = priority_icons.get(priority, "⚪")
signals_str = "\n - ".join(intent_signals) if intent_signals else "None detected"
report = f"""
{'='*55}
{icon} LEAD QUALIFICATION REPORT
{'='*55}
Name: {name}
Email: {email}
Phone: {phone}
Source: {source}
{'─'*55}
SCORE: {score}/100
PRIORITY: {priority.upper()}
Budget: ${budget:,.0f}
Timeline: {timeline}
{'─'*55}
Intent Signals:
- {signals_str}
{'─'*55}
ACTION: {follow_up_action}
TIMING: {follow_up_timing}
Notes: {notes if notes else 'None'}
{'='*55}
"""
return report
This keeps the presentation layer separate from the agent logic. When you're ready to plug this into a real CRM, you replace format_lead_report with an API call — the agent loop stays the same.
Step 5: Test with Sample Leads
Now wire everything together and run it against three realistic leads — one hot, one warm, one cold. This is the section that actually proves the thing works.
lead_qualifier.pydef main():
agent = LeadQualifierAgent()
# Three sample leads covering different priority levels
sample_leads = [
{
"label": "Lead 1 — Hot (Real Estate Buyer)",
"text": (
"Hi, my name is Maria Gonzalez, email [email protected], "
"phone 239-555-0182. I'm looking to buy a waterfront property in Naples. "
"My budget is $850,000 and I need to close within 45 days — I'm relocating "
"for work. I've already been pre-approved by my lender and toured 3 homes "
"last week. Came from your website contact form."
)
},
{
"label": "Lead 2 — Warm (Restaurant Owner)",
"text": (
"Name: David Park, [email protected]. I run a restaurant in Bonita Springs "
"and I'm interested in AI automation for reservations and customer follow-up. "
"Budget is somewhere around $5,000 to $8,000 to start. No hard deadline but "
"ideally within the next quarter. Found you through a referral from another "
"business owner. Want to learn more before committing."
)
},
{
"label": "Lead 3 — Cold (Vague Inquiry)",
"text": (
"Hey I saw your ad. My name is Tom, [email protected]. "
"Just curious what AI stuff costs these days. Not sure if we need it yet. "
"No real budget set. Maybe sometime next year we'd look into it. "
"Came from a Facebook ad."
)
}
]
for lead in sample_leads:
print(f"\nProcessing: {lead['label']}")
print("Running agent...\n")
result = agent.qualify_lead(lead["text"])
report = format_lead_report(result)
print(report)
# Reset agent state between leads
agent.extracted_data = {}
agent.score_data = {}
if __name__ == "__main__":
main()
Run it with python lead_qualifier.py and you'll see output like this:
Processing: Lead 1 — Hot (Real Estate Buyer) Running agent... ======================================================= 🔥 LEAD QUALIFICATION REPORT ======================================================= Name: Maria Gonzalez Email: [email protected] Phone: 239-555-0182 Source: website contact form ------------------------------------------------------- SCORE: 94/100 PRIORITY: HOT Budget: $850,000 Timeline: within 45 days ------------------------------------------------------- Intent Signals: - pre-approved by lender - toured 3 homes last week - relocating for work - hard move deadline ------------------------------------------------------- ACTION: Call within 1 hour. Send top 5 available waterfront listings. Offer same-day showing. TIMING: within 1 hour Notes: High-urgency relocation buyer. Pre-approved. Extremely close to purchase decision. ======================================================= Processing: Lead 2 — Warm (Restaurant Owner) Running agent... ======================================================= 🟡 LEAD QUALIFICATION REPORT ======================================================= Name: David Park Email: [email protected] Phone: N/A Source: referral ------------------------------------------------------- SCORE: 62/100 PRIORITY: WARM Budget: $6,500 Timeline: within the next quarter ------------------------------------------------------- Intent Signals: - specific use case identified (reservations, follow-up) - referred by another business owner - defined budget range ------------------------------------------------------- ACTION: Send case study for restaurant AI automation. Schedule discovery call this week. TIMING: within 24 hours Notes: Decision-maker but needs education. Referral source adds credibility — mention it. ======================================================= Processing: Lead 3 — Cold (Vague Inquiry) Running agent... ======================================================= 🔵 LEAD QUALIFICATION REPORT ======================================================= Name: Tom Email: [email protected] Phone: N/A Source: Facebook ad ------------------------------------------------------- SCORE: 18/100 PRIORITY: COLD Budget: $0 Timeline: not specified ------------------------------------------------------- Intent Signals: - general curiosity about pricing ------------------------------------------------------- ACTION: Add to email nurture sequence. Send introductory pricing guide. TIMING: within 1 week Notes: No budget, no timeline, no specifics. Keep warm with low-effort outreach only. =======================================================
How It Works
The agent sends your raw lead text to Claude with a system prompt that tells it to use two tools in sequence. Claude calls extract_lead_data first, which returns a structured JSON object with every field we care about. Then it calls score_and_recommend using that data to generate a score and action plan.
Your Python loop catches each tool call, runs process_tool_call, and sends the result back to Claude as a tool_result message. Claude sees the result and decides whether to call another tool or stop. When it stops, you've got both outputs stored in the agent object and merged into a single dict.
The scoring logic lives inside Claude's reasoning — you prompted it to weight budget, timeline, and intent signals. But because all outputs go through a strict schema, you always get a clean integer score and a priority enum you can filter or sort on programmatically.
Common Errors and Fixes
AuthenticationError: invalid x-api-keyThis means your API key isn't loading correctly. Double-check that your
.env file is in the same directory as your script and that you called load_dotenv() before initializing the client. Also verify there are no extra spaces around the = in the .env file.
BadRequestError: messages.1.content: Input should be a valid stringThis happens when you append
response.content to messages without including it as the full assistant turn first. Make sure you're appending {"role": "assistant", "content": response.content} before you add your tool results. The order matters — assistant message first, then user tool results.
KeyError: 'score' in format_lead_reportThis means Claude called
extract_lead_data but the loop exited before it called score_and_recommend. Check your loop's break condition — if you're breaking on stop_reason == "end_turn" too early, Claude never gets to the second tool call. Add a print statement inside the loop to see what response.stop_reason actually returns on each iteration.
Next Steps: Adding Multi-Agent Workflows
This agent is a solid foundation. Here's where to take it next.
1. Add a CRM integration. Replace format_lead_report with a POST request to HubSpot, Salesforce, or GoHighLevel. Pass the structured dict directly — all the fields are already named and typed correctly.
2. Build a multi-agent pipeline. Spin up a second Claude agent that drafts a personalized follow-up email based on the extracted lead data. The lead qualifier feeds the email drafter, which feeds a send queue. That's full AI lead follow up automation end to end.
3. Add a webhook endpoint. Wrap qualify_lead in a FastAPI route and deploy it to a VPS or serverless function. Every form submission hits the endpoint and gets qualified in real time — no batch jobs, no delays.
4. Score drift