# How to score leads with Claude and write the score back to HubSpot

> Create custom HubSpot properties, give Claude an explicit rubric, get a structured score back and PATCH it onto the contact. Compare connector, Zapier/n8n, script and Teton.

- Published: 2026-10-28
- URL: https://teton.so/guides/score-leads-with-claude-write-back-to-hubspot

To score leads with Claude and write the score back to HubSpot, create custom contact properties for the score, the reason and the rubric version. Send each lead's properties to Claude with an explicit rubric, and have Claude return a structured result. Then update the contact through HubSpot's CRM API, for example `PATCH /crm/v3/objects/contacts/{id}`. What changes between setups is what runs that loop. For a few leads, Claude with HubSpot's connector is enough. For every new lead, use Zapier, Make or n8n, your own script on a schedule or webhook, or a hosted Claude Code workflow such as [Teton](https://teton.so).

The steps below work the same whichever runner you choose.

## Step 1: Create properties for the AI score

Write to your own custom properties, not to HubSpot's built-in score fields. The AI score stays separate, you can compare it with your existing scoring, and HubSpot workflows can branch on it. Three properties cover most needs:

- `ai_lead_score`: a number from 0 to 100.
- `ai_score_reason`: multi-line text with Claude's one- or two-sentence justification.
- `ai_score_version`: single-line text naming the rubric version that produced the score.

Create them in HubSpot settings or through the properties API. You need a private app token with the `crm.objects.contacts.read`, `crm.objects.contacts.write` and `crm.schemas.contacts.write` scopes.

```bash
curl -s -X POST https://api.hubapi.com/crm/v3/properties/contacts \
  -H "Authorization: Bearer $HUBSPOT_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"ai_lead_score","label":"AI lead score","type":"number","fieldType":"number","groupName":"contactinformation","description":"0-100, written by the Claude scoring job"}'

curl -s -X POST https://api.hubapi.com/crm/v3/properties/contacts \
  -H "Authorization: Bearer $HUBSPOT_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"ai_score_reason","label":"AI score reason","type":"string","fieldType":"textarea","groupName":"contactinformation"}'

curl -s -X POST https://api.hubapi.com/crm/v3/properties/contacts \
  -H "Authorization: Bearer $HUBSPOT_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"ai_score_version","label":"AI score version","type":"string","fieldType":"text","groupName":"contactinformation"}'
```

Get the type right the first time. Claude's HubSpot connector can create properties but [cannot edit or delete them](https://www.vantagepoint.io/blog/hs/claude-for-hubspot-admins), so a wrong type has to be fixed by hand in HubSpot.

## Step 2: Write the rubric down

The common failure is asking Claude to "score this lead" and letting it guess what a good lead is. [Vantagepoint's guide](https://vantagepoint.io/blog/sf/anthropic/automate-lead-qualification-claude-ai-hubspot-workflows) puts it plainly: spell out the rubric. Put it in a file (`rubric.md`) so it can be reviewed and versioned like code:

```text
Lead scoring rubric, version 2026-10-v1

Score 0-100 as the sum of:

- Company size (numemployees): 200-2000 = 30, 50-199 = 20, 2001+ = 15, under 50 = 5, unknown = 10
- Industry: B2B software or fintech = 25, other B2B = 15, B2C or unknown = 5
- Title (jobtitle): RevOps, Sales Ops, GTM or Growth leader = 30; other sales/marketing = 15; other = 5
- Country: US, UK, CA, DE = 15; other = 5

Do not award points for information that is missing. Personal email domains cap the total at 40.
Reason: one or two sentences naming the factors that drove the score.
```

A points-based rubric keeps scores consistent between runs. Claude's judgment then goes into the fuzzy parts, such as reading a job title or inferring an industry, and not into choosing weights.

## Step 3: Get a structured result from Claude

Most tutorials ask Claude to "reply in JSON" and parse the text. It is more reliable to define a tool and force Claude to call it, so the answer always arrives as an object that matches your schema. This script finds contacts with no score yet, scores each one and writes the result back. It needs `pip install anthropic requests` and the environment variables `HUBSPOT_TOKEN` and `ANTHROPIC_API_KEY`.

```python
import os, json, requests, anthropic

HS = "https://api.hubapi.com"
H = {"Authorization": f"Bearer {os.environ['HUBSPOT_TOKEN']}"}
MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-5")
RUBRIC = open("rubric.md").read()
RUBRIC_VERSION = "2026-10-v1"
PROPS = ["email", "jobtitle", "company", "industry", "numemployees", "country"]

TOOL = {
    "name": "record_score",
    "description": "Record the lead score and the reason for it.",
    "input_schema": {
        "type": "object",
        "properties": {
            "score": {"type": "integer", "description": "0-100 per the rubric"},
            "reason": {"type": "string"},
        },
        "required": ["score", "reason"],
    },
}

client = anthropic.Anthropic()

def unscored_contacts(limit=50):
    body = {
        "filterGroups": [{"filters": [
            {"propertyName": "ai_lead_score", "operator": "NOT_HAS_PROPERTY"}]}],
        "properties": PROPS,
        "limit": limit,
    }
    r = requests.post(f"{HS}/crm/v3/objects/contacts/search", headers=H, json=body, timeout=30)
    r.raise_for_status()
    return r.json()["results"]

def score(props):
    msg = client.messages.create(
        model=MODEL,
        max_tokens=500,
        system=RUBRIC,
        tools=[TOOL],
        tool_choice={"type": "tool", "name": "record_score"},
        messages=[{"role": "user", "content": json.dumps(props)}],
    )
    result = next(b.input for b in msg.content if b.type == "tool_use")
    result["score"] = max(0, min(100, int(result["score"])))  # clamp in code, don't trust the schema alone
    return result

def write_back(contact_id, result):
    properties = {
        "ai_lead_score": str(result["score"]),
        "ai_score_reason": result["reason"][:2000],
        "ai_score_version": RUBRIC_VERSION,
    }
    r = requests.patch(f"{HS}/crm/v3/objects/contacts/{contact_id}",
                       headers=H, json={"properties": properties}, timeout=30)
    r.raise_for_status()

for c in unscored_contacts():
    result = score({k: c["properties"].get(k) for k in PROPS})
    write_back(c["id"], result)
    print(c["id"], result["score"])
```

A few notes:

- HubSpot returns property values as strings, and sending the score as a string as well works for a number property. Some guides report a 400 error when sending a bare integer. Test your account before relying on either form.
- The `NOT_HAS_PROPERTY` filter makes the job safe to rerun: contacts that are already scored are skipped. To rescore after a rubric change, filter on `ai_score_version` not equal to the current version.
- Set `ANTHROPIC_MODEL` to whichever current Claude model you want. Smaller models are cheaper per lead. Compare them on the same sample before switching.
- For a one-time backfill of a large database, Anthropic's Message Batches API and HubSpot's batch update endpoint cut the number of calls. HubSpot's API rate limits depend on your subscription, so check them before looping over everything.

## Step 4: Let HubSpot route on the score

Keep routing in HubSpot. A HubSpot workflow enrolled on `ai_lead_score` is known can branch on ranges, for example 80-100 hot, 50-79 warm and 0-49 cold as in [Vantagepoint's setup](https://vantagepoint.io/blog/sf/anthropic/automate-lead-qualification-claude-ai-hubspot-workflows). It can then set lifecycle stage, assign an owner or enroll the contact in a sequence. [Adventure Media's example](https://www.adventuremedia.ai/blog/how-to-connect-claude-code-to-your-crm-and-automate-lead-scoring) uses the same split: the script writes properties, and a HubSpot workflow acts on them. Your sales team can then change routing without touching the scoring code.

## Where to run it

### Claude with HubSpot's connector (a handful of leads)

HubSpot's remote MCP server at `mcp.hubspot.com` lets Claude and other MCP clients read and write CRM records over OAuth, within each user's HubSpot permissions. According to [Coworker](https://coworker.ai/blog/hubspot-mcp), it has been generally available since April 13, 2026. You can paste the rubric into a chat and ask Claude to score and update a list. [HubSpot's docs](https://knowledge.hubspot.com/integrations/set-up-and-use-the-hubspot-connector-for-claude) say Claude may show proposed changes before making them, and actions appear in the HubSpot audit log under both the user and the Claude connector. The limits, [per Supered](https://www.supered.io/blog/hubspot-mcp): 10 records per bulk update, and no deletes. That makes it good for ad hoc scoring and poor for scoring every new lead.

### Zapier, Make or n8n (no code, per new lead)

This is the most common pattern: trigger on a new HubSpot contact, call Claude with the rubric, and map the result to custom properties with an Update Contact step. [n8n's template 16011](https://n8n.io/workflows/16011) creates the score, tier and reason properties if they are missing, researches the company website, has Claude return a score and reason, and sets lifecycle stage by threshold. It needs a HubSpot app token and an Anthropic API key. This fits when the inputs are fixed and someone on the team already maintains these tools.

### Your own script on a schedule or webhook

Run the script above on cron, or call it from a HubSpot workflow webhook when a contact is created or changes. HubSpot's custom code action can also run it inside a workflow on plans that include that feature. You get full control, but you also own the hosting, the secrets, retries and logs.

### Teton (a Claude Code workflow, hosted)

With [Teton](https://teton.so), you score a batch of leads once with Claude Code, in plain language, until the output is right. Then you type `/teton`, and Claude writes the job up as one workflow file with its trigger, steps and credentials. One deploy makes it live on a schedule (cron with a timezone), a webhook or by hand. The steps Claude worked out run the same way each time, and Claude is called only in the scoring step. The rubric can live in a shared context file. Editing it changes the next run without a deploy, and each run records which revision it read, so you can trace any run back to the rules it used. Keep writing `ai_score_version` to HubSpot if you want to report on versions there. Each run lists exactly which CRM records it changed, along with its result and cost. The workspace Postgres database can hold what was already scored. HubSpot credentials are entered once in the dashboard, never in the workflow file.

## Comparison

| | Best for | Trigger | Volume | You maintain |
| --- | --- | --- | --- | --- |
| Claude + HubSpot connector | Scoring a list on request | You, in chat | 10 records per bulk write | Nothing; a person is in the loop |
| Zapier / Make / n8n | Scoring each new contact, no code | New or updated contact | Per-record steps | The zap or flow |
| Own script | Full control, large backfills | Cron or webhook | Batch APIs, your rate limits | Hosting, secrets, retries, logs |
| Teton | Claude Code jobs that run unattended | Schedule, webhook or by hand | Your workflow's design | The workflow and rubric |

## Check the scores against outcomes

An AI score is only useful if it predicts something. After a few weeks, build a HubSpot report of `ai_lead_score` bands against lifecycle stage or closed-won deals, split by `ai_score_version`. If hot leads don't convert better than cold ones, change the rubric, bump the version and rescore. Spot-check `ai_score_reason` on a sample each time, since the reasons show where Claude reads the rubric differently from your team.

## FAQ

### Should I overwrite HubSpot's built-in lead score?

No. Keep the AI score in its own property so you can compare the two, roll back by ignoring one field, and avoid the AI score being overwritten by HubSpot's own scoring.

### How do I rescore when a contact changes?

Trigger on property changes, such as job title, company size or lifecycle stage, through a HubSpot workflow webhook or your automation tool. Or run a scheduled job that rescores contacts whose `ai_score_version` is out of date.

### Why use tool use instead of asking for JSON?

Forcing a tool call gives you an object that matches your schema rather than text you have to parse. Still validate and clamp values in code before writing them to the CRM.

### Can Claude enrich the lead before scoring?

Yes. The n8n template above looks up the company website first. Add enrichment as its own step before scoring, and pass Claude only the fields the rubric uses.

---

- [All guides](https://teton.so/guides)
- [Teton](https://teton.so): the cloud for Claude Code. Build a workflow with Claude once and Teton runs it on a schedule or a webhook.
