Score HubSpot leads based on firmographic and technographic fit using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code or another agent that supports the Agent Skills standard
  • HubSpot private app token stored as HUBSPOT_TOKEN environment variable
  • A custom HubSpot contact property for the fit score (e.g., icp_fit_score, number type, 0-100)

Overview

This approach creates an agent skill that scores (or re-scores) a batch of HubSpot contacts based on firmographic fit. Unlike the automated approaches, this runs on-demand -- useful when you update your scoring model and need to re-score everyone, or when you want to score a specific segment.

Step 1: Create the skill

Create .claude/skills/lead-scoring/SKILL.md:

---
name: lead-scoring
description: Score or re-score HubSpot contacts based on firmographic and technographic fit
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Score HubSpot contacts based on ICP fit. Writes the score to the `icp_fit_score` property.
 
Usage:
- `/lead-scoring` — score all unscored contacts
- `/lead-scoring --all` — re-score every contact (use after changing the scoring model)
 
Run: `python $SKILL_DIR/scripts/score_leads.py $@`

Step 2: Write the script

Create .claude/skills/lead-scoring/scripts/score_leads.py:

#!/usr/bin/env python3
"""Score HubSpot contacts based on firmographic/technographic fit."""
import os, sys, requests
 
HUBSPOT_TOKEN = os.environ.get("HUBSPOT_TOKEN")
if not HUBSPOT_TOKEN:
    print("ERROR: Set HUBSPOT_TOKEN env var")
    sys.exit(1)
 
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
 
# --- Scoring model (edit these to match your ICP) ---
def score_contact(props):
    score = 0
 
    # Company size (0-30)
    emp = int(props.get("numberofemployees") or 0)
    if 200 <= emp <= 2000: score += 30
    elif 50 <= emp < 200: score += 20
    elif 2000 < emp <= 10000: score += 15
    elif emp > 0: score += 5
 
    # Industry (0-25)
    industry = (props.get("industry") or "").lower()
    ideal = ["saas", "technology", "software", "computer software"]
    good = ["financial services", "consulting", "marketing"]
    if industry in ideal: score += 25
    elif industry in good: score += 15
    elif industry: score += 5
 
    # Seniority (0-30)
    title = (props.get("jobtitle") or "").lower()
    if any(t in title for t in ["ceo","cto","cfo","coo","cmo","cro","chief"]): score += 30
    elif any(t in title for t in ["vp","vice president","head of"]): score += 25
    elif "director" in title: score += 20
    elif any(t in title for t in ["manager","lead"]): score += 10
 
    # Source (0-15)
    source = (props.get("hs_analytics_source") or "").lower()
    source_scores = {"organic_search":15,"direct_traffic":12,"referrals":10,"paid_search":8,"social_media":5}
    score += source_scores.get(source, 0)
 
    return min(score, 100)
 
# --- Fetch contacts ---
rescore_all = "--all" in sys.argv
filter_groups = [] if rescore_all else [{"filters": [
    {"propertyName": "icp_fit_score", "operator": "NOT_HAS_PROPERTY"}
]}]
 
contacts = []
after = None
while True:
    body = {
        "properties": ["firstname","lastname","jobtitle","company","numberofemployees","industry","hs_analytics_source"],
        "limit": 100,
    }
    if filter_groups:
        body["filterGroups"] = filter_groups
    if after:
        body["after"] = after
 
    resp = requests.post("https://api.hubapi.com/crm/v3/objects/contacts/search", headers=HEADERS, json=body)
    resp.raise_for_status()
    data = resp.json()
    contacts.extend(data.get("results", []))
    after = data.get("paging", {}).get("next", {}).get("after")
    if not after:
        break
 
if not contacts:
    print("No contacts to score")
    sys.exit(0)
 
print(f"Scoring {len(contacts)} contacts...")
 
# --- Score and update ---
scored = 0
for contact in contacts:
    props = contact["properties"]
    s = score_contact(props)
    requests.patch(
        f"https://api.hubapi.com/crm/v3/objects/contacts/{contact['id']}",
        headers=HEADERS,
        json={"properties": {"icp_fit_score": str(s)}}
    ).raise_for_status()
    name = f"{props.get('firstname','')} {props.get('lastname','')}".strip()
    print(f"  {name}: {s}/100")
    scored += 1
 
print(f"\nDone. Scored {scored} contacts.")

Step 3: Run it

# Score unscored contacts
/lead-scoring
 
# Re-score all contacts after updating the model
/lead-scoring --all

When to use this approach

  • You just updated your ICP or scoring weights and need a bulk re-score
  • You want to score a batch of contacts on demand during pipeline review
  • You're testing different scoring models before deploying an always-on automation

Need help implementing this?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.