DEV Community

Henry Knight
Henry Knight

Posted on

I Built a Lead Scoring Bot with Claude AI (50 Lines of Python)

Manually sorting leads by priority is a 2-hour task. I automated it in 50 lines.

Every week, I was staring at a spreadsheet of 200+ leads — company names, job titles, email engagement data — trying to figure out who to call first. Hot leads buried under cold ones. Revenue sitting in a CSV file, unsorted.

Then I built a 50-line Python script using Claude's tool_use API. Now it runs in 30 seconds and hands me a ranked list with personalized follow-up email drafts ready to copy-paste.

Here's exactly how it works.

What the Bot Does

The script:

  1. Reads a CSV of leads (name, company, title, open_count, click_count, last_activity)
  2. Sends each lead to Claude with a score_lead tool definition
  3. Claude calls the tool with a priority score (1-10) and reasoning
  4. Outputs a ranked list with a personalized follow-up email per lead

No external scoring model. No hardcoded rules. Claude reads the signals and makes the call.

The Full Script

import csv
import json
import anthropic
from jinja2 import Template

client = anthropic.Anthropic()

EMAIL_TEMPLATE = Template("""
Hi {{ first_name }},

{{ opening_line }}

{{ value_prop }}

Worth a quick 15-minute call this week?

Best,
Henry
""")

tools = [
    {
        "name": "score_lead",
        "description": "Score a sales lead based on engagement signals and return priority + email draft components",
        "input_schema": {
            "type": "object",
            "properties": {
                "priority_score": {
                    "type": "integer",
                    "description": "Lead priority from 1 (cold) to 10 (hot)"
                },
                "reasoning": {
                    "type": "string",
                    "description": "Why this lead got this score"
                },
                "opening_line": {
                    "type": "string",
                    "description": "Personalized opening line for follow-up email"
                },
                "value_prop": {
                    "type": "string",
                    "description": "One sentence value prop tailored to their role/company"
                }
            },
            "required": ["priority_score", "reasoning", "opening_line", "value_prop"]
        }
    }
]

def score_lead(lead: dict) -> dict:
    prompt = f"""Score this sales lead based on engagement signals:

Name: {lead['name']}
Company: {lead['company']}
Title: {lead['title']}
Email opens (last 30 days): {lead['open_count']}
Email clicks (last 30 days): {lead['click_count']}
Last activity: {lead['last_activity']}

Call the score_lead tool with your assessment."""

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=500,
        tools=tools,
        messages=[{"role": "user", "content": prompt}]
    )

    for block in response.content:
        if block.type == "tool_use" and block.name == "score_lead":
            return block.input

    return {"priority_score": 0, "reasoning": "No tool call", "opening_line": "", "value_prop": ""}

def main():
    leads = []
    with open("leads.csv", newline="") as f:
        reader = csv.DictReader(f)
        for row in reader:
            result = score_lead(row)
            first_name = row["name"].split()[0]
            email = EMAIL_TEMPLATE.render(
                first_name=first_name,
                opening_line=result["opening_line"],
                value_prop=result["value_prop"]
            )
            leads.append({
                "name": row["name"],
                "company": row["company"],
                "score": result["priority_score"],
                "reasoning": result["reasoning"],
                "email_draft": email
            })

    leads.sort(key=lambda x: x["score"], reverse=True)

    print(f"\n{'='*60}")
    print(f"LEAD PRIORITY RANKING — {len(leads)} leads scored")
    print(f"{'='*60}\n")

    for i, lead in enumerate(leads, 1):
        print(f"#{i} [{lead['score']}/10] {lead['name']} @ {lead['company']}")
        print(f"    Why: {lead['reasoning']}")
        print(f"    Email draft:\n{lead['email_draft']}")
        print()

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Sample leads.csv

name,company,title,open_count,click_count,last_activity
Sarah Chen,Acme Corp,VP of Sales,12,4,2 days ago
Mike Torres,StartupXYZ,Founder,1,0,3 weeks ago
Jessica Park,MegaCo,SDR Manager,8,2,5 days ago
Dan Williams,TechFlow,CTO,0,0,2 months ago
Enter fullscreen mode Exit fullscreen mode

How Claude's tool_use Makes This Work

The key insight: Claude's tool_use forces structured output.

Without tool_use, you'd prompt Claude for JSON and get inconsistent formatting, missing fields, or markdown-wrapped JSON you have to parse around. With tool_use, Claude is required to call your function with a validated schema. You get clean Python dicts every time.

The score_lead tool definition acts as a contract. Claude reads the engagement signals and must return a score between 1-10, reasoning, and email components. No parsing. No validation. Just structured data.

This is the same pattern that makes Claude powerful for document-aware workflows — define the schema for what you want out, let Claude do the reasoning.

Sample Output

Running the script on the CSV above gives something like:

==============================
LEAD PRIORITY RANKING — 4 leads scored
==============================

#1 [9/10] Sarah Chen @ Acme Corp
    Why: 12 opens + 4 clicks in 30 days, VP-level, engaged within 48 hours — hot
    Email draft:
    Hi Sarah,
    Saw you've been exploring our content — hoping it's been useful for the Acme team...

#2 [7/10] Jessica Park @ MegaCo
    Why: Consistent engagement, SDR Manager is a strong buying signal for sales tooling

#3 [2/10] Mike Torres @ StartupXYZ
    Why: Single open, no clicks, low recency — cold for now

#4 [1/10] Dan Williams @ TechFlow
    Why: Zero engagement in 2 months — effectively dead
Enter fullscreen mode Exit fullscreen mode

Running It Yourself

pip install anthropic jinja2
export ANTHROPIC_API_KEY=your_key_here
python lead_scorer.py
Enter fullscreen mode Exit fullscreen mode

Your leads.csv just needs columns: name, company, title, open_count, click_count, last_activity. Adapt column names to match whatever your CRM exports.

What to Build Next

A few extensions that take this further:

Batch processing with Anthropic's Message Batches API — score 1,000 leads overnight at 50% cost reduction instead of running them synchronously.

Auto-send via SendGrid — filter for scores 8+ and pipe the email drafts directly to an outbound sequence.

Webhook trigger — wrap this in a FastAPI endpoint and trigger it whenever a new lead hits your CRM.

The core pattern scales. Claude handles the reasoning; your code handles the plumbing.


I packaged my full browser automation + API toolkit (including this script) here: https://payhip.com/b/GuCSa

If you build something with this, drop a comment — curious what use cases you're hitting.

Top comments (0)