What You'll Build
If you've ever had your sales team waste hours chasing leads that never convert, this tutorial solves that problem. You'll build a working lead scoring agent in Python that uses Claude's tool use feature to automatically evaluate inbound leads and assign them a score from 0 to 100, along with a qualification tier and a recommended next action. By the end, you'll have production-ready code you can drop into any CRM pipeline or sales workflow.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python classes and dictionaries
anthropicPython SDK installed (pip install anthropic)- A terminal and a code editor you're comfortable with
Step 1: Set Up Your Claude API Environment
First things first, let's get the environment wired up. You want to keep your API key out of your source code, so we'll pull it from an environment variable instead of hardcoding it.
Run this in your terminal to install the SDK if you haven't already:
terminalpip install anthropic
Then set your API key as an environment variable:
terminal (Mac/Linux)export ANTHROPIC_API_KEY="sk-ant-your-key-here"
set ANTHROPIC_API_KEY=sk-ant-your-key-here
Now let's verify the client initializes correctly before we write any agent logic:
test_connection.pyimport os
import anthropic
# Initialize the client using the environment variable automatically
client = anthropic.Anthropic()
# Quick ping to confirm the API key works
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=64,
messages=[{"role": "user", "content": "Reply with: API connection successful."}]
)
print(response.content[0].text)
You should see API connection successful. printed in your terminal. If you get an authentication error, double-check that the environment variable is set in the same shell session you're running Python from.
Step 2: Define the Lead Scoring Tool and Criteria
This is the core of how Claude's tool use works. You define a tool as a JSON schema that describes what inputs the model can pass back to your Python code. Think of it like giving Claude a structured form to fill out instead of letting it reply with freeform text.
For lead scoring, we want Claude to evaluate five dimensions: budget fit, authority (is this person a decision-maker?), need, timeline, and company size. Each one gets a weighted sub-score, and the tool returns a combined result.
lead_scoring_tools.pyimport anthropic
# The tool definition tells Claude exactly what fields to return
LEAD_SCORING_TOOL = {
"name": "score_lead",
"description": (
"Evaluate a sales lead and return a structured score based on budget fit, "
"decision-making authority, business need, purchase timeline, and company size. "
"Use the lead information provided to fill out every field accurately."
),
"input_schema": {
"type": "object",
"properties": {
"score": {
"type": "integer",
"description": "Overall lead score from 0 to 100. Higher means stronger fit.",
"minimum": 0,
"maximum": 100
},
"tier": {
"type": "string",
"enum": ["Hot", "Warm", "Cold", "Disqualified"],
"description": "Qualification tier based on overall score."
},
"budget_score": {
"type": "integer",
"description": "Budget alignment score from 0 to 25.",
"minimum": 0,
"maximum": 25
},
"authority_score": {
"type": "integer",
"description": "Decision-making authority score from 0 to 25.",
"minimum": 0,
"maximum": 25
},
"need_score": {
"type": "integer",
"description": "Strength of business need score from 0 to 25.",
"minimum": 0,
"maximum": 25
},
"timeline_score": {
"type": "integer",
"description": "Purchase timeline urgency score from 0 to 25.",
"minimum": 0,
"maximum": 25
},
"reasoning": {
"type": "string",
"description": "2-3 sentence plain-English explanation of why this lead received this score."
},
"recommended_action": {
"type": "string",
"description": "The single best next action for the sales team to take with this lead."
}
},
"required": [
"score", "tier", "budget_score", "authority_score",
"need_score", "timeline_score", "reasoning", "recommended_action"
]
}
}
Step 3: Create the Agent with Tool Use
Now we build the main agent class. This is where the real work happens. The agent takes a lead, sends it to Claude with the scoring tool attached, and then processes whatever Claude decides to do, whether that's calling the tool directly or asking for clarification first.
The system prompt matters a lot here. You want Claude to act like a senior sales analyst who has seen thousands of leads, not like a generic assistant.
lead_scoring_agent.pyimport os
import json
import anthropic
from lead_scoring_tools import LEAD_SCORING_TOOL
SYSTEM_PROMPT = """You are a senior B2B sales analyst with 15 years of experience
qualifying inbound leads. Your job is to evaluate each lead objectively using the
score_lead tool. Always call the tool — never reply with plain text. Base your
scoring strictly on the information provided. If a field is missing, score it
conservatively (low). Do not inflate scores to be encouraging."""
class LeadScoringAgent:
def __init__(self):
# Client picks up ANTHROPIC_API_KEY from the environment automatically
self.client = anthropic.Anthropic()
self.model = "claude-sonnet-4-6"
self.tools = [LEAD_SCORING_TOOL]
def _format_lead_message(self, lead: dict) -> str:
"""Convert a lead dictionary into a readable message for Claude."""
lines = ["Please score the following sales lead:"]
field_labels = {
"name": "Name",
"company": "Company",
"title": "Job Title",
"email": "Email",
"budget": "Stated Budget",
"timeline": "Purchase Timeline",
"employees": "Company Size (employees)",
"industry": "Industry",
"pain_points": "Stated Pain Points",
"source": "Lead Source",
"notes": "Additional Notes"
}
for key, label in field_labels.items():
if key in lead:
lines.append(f"{label}: {lead[key]}")
return "\n".join(lines)
def score_lead(self, lead: dict) -> dict:
"""
Send a lead to Claude and return the structured scoring result.
Raises ValueError if Claude does not call the scoring tool.
"""
user_message = self._format_lead_message(lead)
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=SYSTEM_PROMPT,
tools=self.tools,
messages=[{"role": "user", "content": user_message}]
)
# Check why Claude stopped generating
if response.stop_reason == "tool_use":
# Find the tool_use block in the response content
for block in response.content:
if block.type == "tool_use" and block.name == "score_lead":
return block.input # This is already a parsed dict
raise ValueError(
f"Claude did not call the score_lead tool. "
f"Stop reason: {response.stop_reason}. "
f"Response: {response.content}"
)
Step 4: Build the Run Loop and Process Leads in Bulk
A single lead scoring call is useful, but the real value comes from processing a batch of leads and getting a ranked list back. Let's build a run loop that takes a list of leads, scores each one, and sorts the results by score so your sales team knows exactly who to call first.
We'll also add basic error handling so one bad lead doesn't crash the whole batch.
run_scoring.pyimport json
import anthropic
from lead_scoring_agent import LeadScoringAgent
def process_leads(leads: list[dict]) -> list[dict]:
"""
Score a list of leads and return them sorted by score (highest first).
Each result dict contains the original lead data merged with scoring output.
"""
agent = LeadScoringAgent()
results = []
for i, lead in enumerate(leads):
print(f"Scoring lead {i + 1}/{len(leads)}: {lead.get('name', 'Unknown')}...")
try:
score_result = agent.score_lead(lead)
# Merge original lead info with the scoring output
combined = {**lead, **score_result}
results.append(combined)
except ValueError as e:
print(f" Warning: Could not score lead {lead.get('name')}. Error: {e}")
# Append with a placeholder so we don't lose the lead record
results.append({**lead, "score": 0, "tier": "Error", "reasoning": str(e)})
# Sort by score descending so the hottest leads appear first
results.sort(key=lambda x: x.get("score", 0), reverse=True)
return results
def print_results(results: list[dict]) -> None:
"""Print a clean summary of scored leads to the console."""
print("\n" + "=" * 60)
print("LEAD SCORING RESULTS (Ranked by Score)")
print("=" * 60)
for lead in results:
print(f"\nName: {lead.get('name', 'N/A')}")
print(f"Company: {lead.get('company', 'N/A')}")
print(f"Score: {lead.get('score', 0)}/100 | Tier: {lead.get('tier', 'N/A')}")
print(f"Reasoning: {lead.get('reasoning', 'N/A')}")
print(f"Next Action: {lead.get('recommended_action', 'N/A')}")
print(f"Sub-scores: Budget={lead.get('budget_score', 0)} | "
f"Authority={lead.get('authority_score', 0)} | "
f"Need={lead.get('need_score', 0)} | "
f"Timeline={lead.get('timeline_score', 0)}")
print("-" * 60)
if __name__ == "__main__":
# Import the sample leads defined in the next step
from sample_leads import SAMPLE_LEADS
results = process_leads(SAMPLE_LEADS)
print_results(results)
# Optionally save results to JSON for downstream CRM import
with open("scored_leads.json", "w") as f:
json.dump(results, f, indent=2)
print("\nResults saved to scored_leads.json")
Step 5: Test with Real Lead Data
Let's use three realistic leads that represent the range you'd actually see: one strong prospect, one borderline case, and one that should be disqualified early. This gives you a good smoke test for whether your scoring criteria are calibrated correctly.
sample_leads.py# Realistic sample leads for testing the scoring agent
SAMPLE_LEADS = [
{
"name": "Maria Gonzalez",
"company": "Gulf Coast Properties LLC",
"title": "CEO",
"email": "[email protected]",
"budget": "$15,000 - $25,000 for AI automation tools",
"timeline": "Want to launch before Q1 next year (90 days)",
"employees": "45",
"industry": "Real Estate",
"pain_points": (
"Manually entering MLS listings takes 3 hours per agent per week. "
"Lead follow-up is inconsistent. No CRM automation in place."
),
"source": "Website contact form",
"notes": "Already approved budget with board. Comparing 2 vendors."
},
{
"name": "Derek Thompson",
"company": "Thompson Auto Group",
"title": "Marketing Coordinator",
"email": "[email protected]",
"budget": "Not sure yet, maybe a few thousand",
"timeline": "Sometime next year, no rush",
"employees": "120",
"industry": "Car Dealership",
"pain_points": "Our website doesn't rank well and we're losing leads to competitors.",
"source": "LinkedIn ad click",
"notes": "Said he'd need to get his GM involved before any decisions."
},
{
"name": "anonymous",
"company": "Unknown",
"title": "Not provided",
"email": "[email protected]",
"budget": "Free or very cheap",
"timeline": "Just browsing",
"employees": "1",
"industry": "Unknown",
"pain_points": "General curiosity about AI",
"source": "Blog comment",
"notes": "No phone number provided. Email appears to be temporary."
}
]
Run the full pipeline with:
terminalpython run_scoring.py
Here's what the actual output looks like when you run this against Claude:
sample outputScoring lead 1/3: Maria Gonzalez...
Scoring lead 2/3: Derek Thompson...
Scoring lead 3/3: anonymous...
============================================================
LEAD SCORING RESULTS (Ranked by Score)
============================================================
Name: Maria Gonzalez
Company: Gulf Coast Properties LLC
Score: 88/100 | Tier: Hot
Reasoning: Maria is the CEO with board-approved budget in the $15K-$25K range and a
concrete 90-day timeline, which indicates serious purchase intent. The pain
points are specific and directly addressable. The only risk is active
vendor comparison, so speed of follow-up is critical.
Next Action: Schedule a discovery call within 24 hours and send a capability overview
deck focused on real estate listing automation.
Sub-scores: Budget=22 | Authority=25 | Need=22 | Timeline=19
------------------------------------------------------------
Name: Derek Thompson
Company: Thompson Auto Group
Score: 38/100 | Tier: Cold
Reasoning: Derek is a marketing coordinator without purchasing authority, has no
defined budget, and has a vague timeline. However, the company is a
legitimate 120-person dealership with a real problem. The opportunity
exists if he can connect us to the GM.
Next Action: Send a low-pressure email with a case study relevant to auto dealerships
and ask if he can loop in the GM for a 20-minute call.
Sub-scores: Budget=5 | Authority=8 | Need=15 | Timeline=10
------------------------------------------------------------
Name: anonymous
Company: Unknown
Score: 3/100 | Tier: Disqualified
Reasoning: No identifying information, a temporary email address, no budget, and
no stated need beyond general curiosity. There is no viable sales
opportunity here at this time.
Next Action: Add to a nurture email sequence only. Do not assign to a sales rep.
Sub-scores: Budget=1 | Authority=1 | Need=1 | Timeline=0
------------------------------------------------------------
Results saved to scored_leads.jsonHow It Works
Here's the plain-English version of what's happening under the hood. When you call agent.score_lead(lead), the agent formats the lead as a natural language message and sends it to Claude along with the tool definition. Claude reads the lead, reasons about it internally, and then decides to "call" the score_lead tool by returning a structured JSON payload that matches your schema.
Your Python code intercepts that tool call, pulls out the block.input dict, and that's your score. Claude never replies with freeform text in this flow because the system prompt instructs it to always use the tool, and the tool schema gives it a clear structure to fill out.
The run loop in run_scoring.py just iterates over your lead list, calls the agent for each one, catches any errors gracefully, and sorts everything by score. The output is a plain Python list of dicts you can pipe into a database, a CRM webhook, or a CSV export with a few extra lines.
asyncio and making concurrent API calls to speed things up. The Anthropic SDK supports async with anthropic.AsyncAnthropic().
Common Errors and Fixes
Error 1: AuthenticationError on startup
anthropic.AuthenticationError: {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}Fix: Your API key isn't being picked up from the environment. Run echo $ANTHROPIC_API_KEY in your terminal to confirm it's set. If the variable is empty, re-export it in the same terminal session where you're running Python. You can also pass the key directly as anthropic.Anthropic(api_key="sk-ant-...") for debugging, but don't commit that to source control.
Error 2: ValueError because Claude returned text instead of a tool call
ValueError: Claude did not call the score_lead tool. Stop reason: end_turn.
Fix: This usually means the system prompt wasn't strong enough to force tool use, or the lead message was so short that Claude responded conversationally. Make sure your system prompt explicitly says "Always call the tool, never reply with plain text." You can also set tool_choice={"type": "tool", "name": "score_lead"} in your messages.create() call to force Claude to use that specific tool every time.
Error 3: KeyError when accessing score results
KeyError: 'budget_score'
Fix: The tool schema has budget_score in the required array, so this shouldn't happen unless you modified the schema and forgot to update the required list. Check that every field you access in print_results() is listed under "required" in your tool's input_schema. Using .get("budget_score", 0) is also a safe fallback for optional fields.
Next Steps
- Connect to your CRM via webhook. Post the scored lead JSON to HubSpot, Salesforce, or GoHighLevel using the
requestslibrary so scores appear automatically inside your existing workflow. - Add async processing for high-volume batches. Swap
anthropic.Anthropic()foranthropic.AsyncAnthropic()and useasyncio.gather()to score dozens of leads in parallel instead of one at a time. - Customize scoring weights per industry. A real estate lead and a manufacturing lead shouldn't be scored the same way. Pass industry-specific scoring instructions into the system prompt dynamically based on the lead's industry field.
- Log scores to a database and track accuracy. Store every score in a Postgres or SQLite table alongside the eventual deal outcome so you can measure whether the agent's predictions match reality over time and tune the criteria accordingly.
Naples AI