If you're manually reviewing leads one by one, you're leaving money and time on the table. This tutorial shows you exactly how to build a multi-agent lead scoring system in Python using the Claude API — the same kind of system we build for real estate agencies, car dealerships, and healthcare practices here in Southwest Florida.
By the end, you'll have a working two-agent pipeline that scores incoming leads, flags high-priority prospects, and outputs structured qualification data — automatically, at scale.
The complete working code is built step-by-step in the sections below. Each snippet is self-contained and builds on the previous one. By Step 5, you'll have the full system running. No pseudocode, no placeholders — just copy, configure, and run.
What You'll Build
You're building a two-agent lead scoring pipeline that takes raw lead data (name, company, budget, intent signals) and returns a structured score from 0–100 with a qualification tier and recommended next action. The first agent scores the lead using custom tools, and the second agent verifies that score for accuracy.
This system can process 100+ leads per day without anyone touching a spreadsheet. It's production-ready and plugs directly into any CRM or webhook pipeline.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python functions and dictionaries
anthropicSDK installed:pip install anthropic- Optional but helpful: basic understanding of JSON
Step 1: Set Up Your Claude API Environment
First, install the Anthropic SDK and configure your API key. I always recommend storing keys in environment variables — never hardcode them in your source files.
setup_env.shpip install anthropic export ANTHROPIC_API_KEY="your-api-key-here"
Now let's verify your connection works and initialize the client we'll use throughout the project.
agent_init.pyimport os
import json
import anthropic
# Initialize the Anthropic client using the environment variable
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
# Model we'll use across both agents
MODEL = "claude-sonnet-4-6"
# Quick sanity check — remove this after confirming it works
def test_connection():
response = client.messages.create(
model=MODEL,
max_tokens=64,
messages=[{"role": "user", "content": "Reply with: Connection successful."}]
)
print(response.content[0].text)
if __name__ == "__main__":
test_connection()
Run python agent_init.py and you should see "Connection successful." printed to the terminal. If you get an authentication error, double-check that your environment variable is set in the same shell session.
Step 2: Define Lead Qualification Tools
This is where the real logic lives. We define three tools that our scoring agent can call: one to evaluate budget fit, one to score intent signals, and one to calculate a final composite score.
Claude uses these tool definitions to decide what to call and when — think of them as the agent's skill set.
lead_tools.pyimport anthropic
# Tool definitions passed to Claude during agent initialization
LEAD_QUALIFICATION_TOOLS = [
{
"name": "evaluate_budget_fit",
"description": (
"Evaluates whether a lead's stated budget aligns with the service tier. "
"Returns a budget score from 0 to 30 and a fit label."
),
"input_schema": {
"type": "object",
"properties": {
"stated_budget": {
"type": "number",
"description": "The lead's stated monthly or project budget in USD"
},
"service_tier": {
"type": "string",
"description": "The service tier being evaluated: starter, growth, or enterprise"
}
},
"required": ["stated_budget", "service_tier"]
}
},
{
"name": "score_intent_signals",
"description": (
"Scores the strength of a lead's intent based on their behavior signals. "
"Returns an intent score from 0 to 40."
),
"input_schema": {
"type": "object",
"properties": {
"requested_demo": {
"type": "boolean",
"description": "Whether the lead explicitly requested a demo or call"
},
"visited_pricing_page": {
"type": "boolean",
"description": "Whether the lead visited the pricing page"
},
"inbound_vs_outbound": {
"type": "string",
"description": "Whether this lead is inbound or outbound"
},
"message_specificity": {
"type": "string",
"description": "How specific their inquiry was: vague, moderate, or detailed"
}
},
"required": ["requested_demo", "visited_pricing_page", "inbound_vs_outbound", "message_specificity"]
}
},
{
"name": "calculate_composite_score",
"description": (
"Combines sub-scores into a final lead score from 0 to 100, assigns a "
"qualification tier (cold, warm, hot, priority), and recommends a next action."
),
"input_schema": {
"type": "object",
"properties": {
"budget_score": {
"type": "number",
"description": "The budget fit score (0-30)"
},
"intent_score": {
"type": "number",
"description": "The intent signal score (0-40)"
},
"company_size_score": {
"type": "number",
"description": "A score for company size and fit (0-30)"
}
},
"required": ["budget_score", "intent_score", "company_size_score"]
}
}
]
# Actual Python functions that execute when Claude calls a tool
def evaluate_budget_fit(stated_budget: float, service_tier: str) -> dict:
thresholds = {
"starter": (500, 2000),
"growth": (2000, 8000),
"enterprise": (8000, 999999)
}
low, high = thresholds.get(service_tier, (0, 0))
if stated_budget >= high * 0.75:
return {"budget_score": 30, "fit_label": "strong_fit"}
elif stated_budget >= low:
return {"budget_score": 18, "fit_label": "possible_fit"}
else:
return {"budget_score": 5, "fit_label": "poor_fit"}
def score_intent_signals(
requested_demo: bool,
visited_pricing_page: bool,
inbound_vs_outbound: str,
message_specificity: str
) -> dict:
score = 0
score += 15 if requested_demo else 0
score += 10 if visited_pricing_page else 0
score += 10 if inbound_vs_outbound == "inbound" else 3
specificity_map = {"detailed": 5, "moderate": 3, "vague": 0}
score += specificity_map.get(message_specificity, 0)
return {"intent_score": min(score, 40)}
def calculate_composite_score(
budget_score: float,
intent_score: float,
company_size_score: float
) -> dict:
total = budget_score + intent_score + company_size_score
if total >= 80:
tier = "priority"
action = "Call within 1 hour"
elif total >= 60:
tier = "hot"
action = "Send personalized proposal today"
elif total >= 40:
tier = "warm"
action = "Add to nurture sequence"
else:
tier = "cold"
action = "Add to general newsletter"
return {
"composite_score": round(total, 1),
"qualification_tier": tier,
"recommended_action": action
}
# Dispatcher — maps tool names to their Python functions
def execute_tool(tool_name: str, tool_input: dict) -> str:
if tool_name == "evaluate_budget_fit":
result = evaluate_budget_fit(**tool_input)
elif tool_name == "score_intent_signals":
result = score_intent_signals(**tool_input)
elif tool_name == "calculate_composite_score":
result = calculate_composite_score(**tool_input)
else:
result = {"error": f"Unknown tool: {tool_name}"}
return json.dumps(result)
import json
Breaking scoring into sub-tools gives Claude the ability to reason about each dimension independently before combining them. It also makes the logic easier to debug — you can see exactly which sub-score drove the final result.
Step 3: Create Your Primary Scoring Agent
The scoring agent takes a raw lead dictionary and runs it through the tool loop. Claude decides which tools to call, in what order, and how to interpret the results.
This is the core of the agentic loop pattern — Claude keeps calling tools until it has enough information to give a final answer.
scoring_agent.pyimport os
import json
import anthropic
from lead_tools import LEAD_QUALIFICATION_TOOLS, execute_tool
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"
SCORING_SYSTEM_PROMPT = """
You are a lead qualification specialist. When given a lead profile, you must:
1. Call evaluate_budget_fit to assess their budget against the appropriate service tier.
2. Call score_intent_signals to evaluate how interested they actually are.
3. Estimate a company_size_score (0-30) based on employee count and company type — use your judgment.
4. Call calculate_composite_score with all three sub-scores to get the final result.
5. Return a clean JSON summary of the scored lead.
Always complete all four steps. Do not skip any tool calls.
"""
def run_scoring_agent(lead: dict) -> dict:
"""
Runs the primary scoring agent on a single lead dictionary.
Returns the structured scoring result from Claude.
"""
user_message = f"Score this lead:\n{json.dumps(lead, indent=2)}"
messages = [{"role": "user", "content": user_message}]
# Agentic loop — keeps running until Claude stops calling tools
while True:
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system=SCORING_SYSTEM_PROMPT,
tools=LEAD_QUALIFICATION_TOOLS,
messages=messages
)
# Append Claude's response to the conversation history
messages.append({"role": "assistant", "content": response.content})
# If Claude is done using tools, extract the final text response
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
try:
return json.loads(block.text)
except json.JSONDecodeError:
return {"raw_response": block.text}
break
# Process each tool call Claude wants to make
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": tool_result
})
# If no tool calls were found despite stop reason being tool_use, exit
if not tool_results:
break
# Add tool results back into the conversation so Claude can continue
messages.append({"role": "user", "content": tool_results})
return {"error": "Agent loop exited without a final response"}
if __name__ == "__main__":
# Example lead input
sample_lead = {
"name": "Maria Gonzalez",
"company": "Gulf Coast Realty Group",
"employees": 45,
"stated_budget_usd": 4500,
"service_interest": "AI chatbot and listing automation",
"requested_demo": True,
"visited_pricing_page": True,
"lead_source": "inbound",
"message": "We have 3 agents struggling with lead follow-up. Need something automated ASAP."
}
result = run_scoring_agent(sample_lead)
print(json.dumps(result, indent=2))
Here's the kind of output you'll see when you run this:
sample_output.json{
"lead_name": "Maria Gonzalez",
"company": "Gulf Coast Realty Group",
"budget_score": 18,
"intent_score": 38,
"company_size_score": 22,
"composite_score": 78.0,
"qualification_tier": "hot",
"recommended_action": "Send personalized proposal today",
"scoring_notes": "Strong inbound intent with detailed use case. Budget fits growth tier. Mid-size team — good expansion potential."
}
Step 4: Build the Verification Agent
A single agent can be overconfident. The verification agent acts as a second pair of eyes — it reviews the primary score and either confirms it or flags it for human review.
This is the pattern that takes a system from "demo project" to something you'd actually trust with real sales data.
verification_agent.pyimport os
import json
import anthropic
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"
VERIFICATION_SYSTEM_PROMPT = """
You are a senior sales analyst reviewing AI-generated lead scores for accuracy.
Given the original lead data and the scoring agent's output, you must:
1. Check if the composite score is internally consistent with the sub-scores.
2. Flag any red flags the scoring agent may have missed (e.g., very small budget but claims enterprise need).
3. Confirm or adjust the qualification tier if needed.
4. Return a JSON object with: verified (true/false), adjusted_tier (if changed), confidence (high/medium/low), and review_notes.
Be concise and direct. Do not re-score — only verify.
"""
def run_verification_agent(lead: dict, scoring_result: dict) -> dict:
"""
Reviews the scoring agent's output against the original lead data.
Returns a verification summary.
"""
user_message = (
f"Original lead data:\n{json.dumps(lead, indent=2)}\n\n"
f"Scoring agent output:\n{json.dumps(scoring_result, indent=2)}\n\n"
"Please verify this score."
)
response = client.messages.create(
model=MODEL,
max_tokens=512,
system=VERIFICATION_SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_message}]
)
raw_text = response.content[0].text
try:
return json.loads(raw_text)
except json.JSONDecodeError:
return {"raw_verification": raw_text}
if __name__ == "__main__":
# Simulated inputs for standalone testing
sample_lead = {
"name": "Maria Gonzalez",
"company": "Gulf Coast Realty Group",
"employees": 45,
"stated_budget_usd": 4500,
"service_interest": "AI chatbot and listing automation",
"requested_demo": True,
"visited_pricing_page": True,
"lead_source": "inbound",
"message": "We have 3 agents struggling with lead follow-up. Need something automated ASAP."
}
sample_score = {
"composite_score": 78.0,
"qualification_tier": "hot",
"budget_score": 18,
"intent_score": 38,
"company_size_score": 22,
"recommended_action": "Send personalized proposal today"
}
verification = run_verification_agent(sample_lead, sample_score)
print(json.dumps(verification, indent=2))
Step 5: Implement the Orchestration Loop
Now we wire both agents together. The orchestrator feeds leads to the scoring agent, passes results to the verification agent, and outputs a final enriched record for each lead.
This is the file you'd actually run in production — or hook into a cron job, a webhook, or a CRM integration.
orchestrator.pyimport os
import json
import time
from scoring_agent import run_scoring_agent
from verification_agent import run_verification_agent
# Sample batch of leads — in production, pull these from your CRM or database
SAMPLE_LEADS = [
{
"name": "Maria Gonzalez",
"company": "Gulf Coast Realty Group",
"employees": 45,
"stated_budget_usd": 4500,
"service_interest": "AI chatbot and listing automation",
"requested_demo": True,
"visited_pricing_page": True,
"lead_source": "inbound",
"message": "We have 3 agents struggling with lead follow-up. Need something automated ASAP."
},
{
"name": "Derek Thornton",
"company": "Thornton Auto Group",
"employees": 110,
"stated_budget_usd": 1200,
"service_interest": "AI customer service chatbot",
"requested_demo": False,
"visited_pricing_page": False,
"lead_source": "outbound",
"message": "Just browsing options, not sure what we need yet."
},
{
"name": "Sandra Patel",
"company": "Naples Orthopedic Partners",
"employees": 28,
"stated_budget_usd": 9500,
"service_interest": "Patient intake automation and predictive scheduling",
"requested_demo": True,
"visited_pricing_page": True,
"lead_source": "inbound",
"message": "We need to reduce front desk workload by at least 40%. Ready to move quickly."
}
]
def process_lead_batch(leads: list) -> list:
"""
Runs each lead through the full two-agent pipeline and returns enriched results.
"""
results = []
for i, lead in enumerate(leads):
print(f"\n[{i+1}/{len(leads)}] Processing: {lead['name']} — {lead['company']}")
# Step 1: Score the lead with the primary agent
score = run_scoring_agent(lead)
print(f" ✓ Scored: {score.get('composite_score', 'N/A')} — {score.get('qualification_tier', 'N/A')}")
# Step 2: Verify the score with the second agent
verification = run_verification_agent(lead, score)
print(f" ✓ Verified: {verification.get('verified', 'N/A')} — Confidence: {verification.get('confidence', 'N/A')}")
# Merge everything into one output record
enriched_lead = {
"lead": lead,
"score": score,
"verification": verification
}
results.append(enriched_lead)
# Avoid rate limiting on large batches
time.sleep(0.5)
return results
def save_results(results: list, output_file: str = "scored_leads.json"):
with open(output_file, "w") as f:
json.dump(results, f, indent=2)
print(f"\n✅ Results saved to {output_file}")
if __name__ == "__main__":
print("Starting lead scoring pipeline...\n")
scored = process_lead_batch(SAMPLE_LEADS)
save_results(scored)
# Print a quick summary
print("\n--- SCORING SUMMARY ---")
for item in scored:
lead = item["lead"]
score = item["score"]
verification = item["verification"]
tier = verification.get("adjusted_tier") or score.get("qualification_tier", "unknown")
print(f"{lead['name']:25} | Score: {score.get('composite_score', 'N/A'):5} | Tier: {tier:10} | Verified: {verification.get('verified', 'N/A')}")
Here's the terminal output you'd see running this against the three sample leads:
terminal_output.txtStarting lead scoring pipeline...
[1/3] Processing: Maria Gonzalez — Gulf Coast Realty Group
✓ Scored: 78.0 — hot
✓ Verified: True — Confidence: high
[2/3