Skill Tracer
Skills › Knowledge Work Plugins › lead-triage › hubspot-scoring.md

hubspot-scoring.md

169 lines · small-business/skills/lead-triage/reference/hubspot-scoring.md at 2574e29

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# HubSpot Scoring — lead-triage
2
3Field names, scoring weights, and ICP defaults.
4
5---
6

HubSpot Scoring — lead-triage

Field names, scoring weights, and ICP defaults.


Raw lines 1 to 6
7## Fields to pull
8
9| HubSpot field | Used for |
10|---|---|
11| `firstname`, `lastname` | Display |
12| `company` | Display. Usually empty — prefer the associated company's `name`, below. |
13| `email` | Follow-up draft recipient |
14| `lifecyclestage` | Filter (keep Lead, MQL) |
15| `hs_lead_status` | Filter (exclude Unqualified) + tie-break |
16| `jobtitle` | Authority tie-break |
17| `hs_buying_role` | Authority tie-break |
18| `num_associated_deals` | Tie-break — a lead with pipeline attached outranks one without |
19| `hs_is_unworked` | Tie-break — `true` means nobody has touched it yet |
20| `createdate` | Urgency — lead age |
21| `notes_last_updated` | Recency penalty. HubSpot labels this "Last Activity Date". There is no `hs_last_activity_date` on the contact object — do not look for one. |
22| `num_notes` | Pre-filter for the note-body fetch below — a count alone cannot fire the keyword signal |
23| `hs_sales_email_last_replied` | Engagement — reply signal |
24| `hs_email_open` | Engagement — open count (use only if within 30 days) |
25| `hs_analytics_last_visit_timestamp` | Engagement — site visit |
26| `num_contacted_notes` | Engagement — outreach volume |

Fields to pull

HubSpot fieldUsed for
firstname, lastnameDisplay
companyDisplay. Usually empty — prefer the associated company's name, below.
emailFollow-up draft recipient
lifecyclestageFilter (keep Lead, MQL)
hs_lead_statusFilter (exclude Unqualified) + tie-break
jobtitleAuthority tie-break
hs_buying_roleAuthority tie-break
num_associated_dealsTie-break — a lead with pipeline attached outranks one without
hs_is_unworkedTie-break — true means nobody has touched it yet
createdateUrgency — lead age
notes_last_updatedRecency penalty. HubSpot labels this "Last Activity Date". There is no hs_last_activity_date on the contact object — do not look for one.
num_notesPre-filter for the note-body fetch below — a count alone cannot fire the keyword signal
hs_sales_email_last_repliedEngagement — reply signal
hs_email_openEngagement — open count (use only if within 30 days)
hs_analytics_last_visit_timestampEngagement — site visit
num_contacted_notesEngagement — outreach volume
Raw lines 7 to 27
28### Company fit fields live on the COMPANY record, not the contact
30`industry` and `numemployees` exist on the contact object but are almost never
31filled in. The populated copies live on the company associated with the contact.
32Pull those instead:
34| COMPANY field | Used for |
35|---|---|
36| `name` | Display — the real company name |
37| `industry` | Company fit |
38| `numberofemployees` | Company fit — exact headcount |
39| `hs_employee_range` | Company fit — fallback when `numberofemployees` is empty |
41Fetch the associated company for each lead, then read fit from there. Only fall
42back to the contact-level `industry` / `numemployees` if there is no associated
43company at all.

Company fit fields live on the COMPANY record, not the contact

industry and numemployees exist on the contact object but are almost never filled in. The populated copies live on the company associated with the contact. Pull those instead:

COMPANY fieldUsed for
nameDisplay — the real company name
industryCompany fit
numberofemployeesCompany fit — exact headcount
hs_employee_rangeCompany fit — fallback when numberofemployees is empty

Fetch the associated company for each lead, then read fit from there. Only fall back to the contact-level industry / numemployees if there is no associated company at all.

Raw lines 28 to 44
45### The enrichment leg: when there is no associated company
47When every lead in a portal has no company
48association, company fit scores 5 across the board and the dimension
49contributes nothing. With a lead-data connector connected — Apollo or Clay,
50peers per `../../../shared/connector-neutrality.md` — fill the gap from the
51lead's email domain instead of scoring it flat:
531. Collect the leads with no associated company and a business email domain
54 (skip gmail, outlook, yahoo, icloud and the like — nothing to enrich).
552. **Say the count and ask once.** Enrichment is metered: each row spends
56 credits. *"32 leads have no company on file. Enriching them from their
57 email domains costs roughly 32 credits in Clay. Go ahead?"* Run only on a
58 yes for this run; a no means the flat ladder below applies to those rows.
593. Enrich by domain:
60 - **Clay:** `find-and-enrich-company` per domain, reading industry and
61 employee count from the returned company data points. Batch through
62 `find-and-enrich-list-of-contacts` when the connector offers it for the
63 whole set.
64 - **Apollo:** `apollo_organizations_enrich` per domain, reading
65 `industry` and `estimated_num_employees`.
664. Score company fit from the enriched values using the same table as an
67 associated company. Mark those rows "fit from enrichment" in the caveats.
685. **Never write the enriched fields back to the CRM from this skill.**
69 Offer it as a `crm-autopilot` hygiene run afterward, behind its write
70 gate, if the owner wants the company records created.
72Without a lead-data connector, or on a no, the flat-score ladder stands.

The enrichment leg: when there is no associated company

When every lead in a portal has no company association, company fit scores 5 across the board and the dimension contributes nothing. With a lead-data connector connected — Apollo or Clay, peers per ../../../shared/connector-neutrality.md — fill the gap from the lead's email domain instead of scoring it flat:

  1. Collect the leads with no associated company and a business email domain (skip gmail, outlook, yahoo, icloud and the like — nothing to enrich).
  2. Say the count and ask once. Enrichment is metered: each row spends credits. "32 leads have no company on file. Enriching them from their email domains costs roughly 32 credits in Clay. Go ahead?" Run only on a yes for this run; a no means the flat ladder below applies to those rows.
  3. Enrich by domain:
  4. Clay: find-and-enrich-company per domain, reading industry and employee count from the returned company data points. Batch through find-and-enrich-list-of-contacts when the connector offers it for the whole set.
  5. Apollo: apollo_organizations_enrich per domain, reading industry and estimated_num_employees.
  6. Score company fit from the enriched values using the same table as an associated company. Mark those rows "fit from enrichment" in the caveats.
  7. Never write the enriched fields back to the CRM from this skill. Offer it as a crm-autopilot hygiene run afterward, behind its write gate, if the owner wants the company records created.

Without a lead-data connector, or on a no, the flat-score ladder stands.

Raw lines 45 to 73
Names a skill crm-autopilot :69
74### Fetch note bodies for the urgency scan
76The urgency keywords live in note *bodies*, and note bodies are engagements —
77they never arrive with the contact property pull. `num_notes` only says a note
78exists; the +15 keyword signal cannot fire unless the bodies are actually read.
80How to do it without flooding the API:
821. Score every lead first *without* the note-keyword signal (provisional score).
832. Fetch associated notes only for the top ~25 leads by provisional score — or
84 for every lead with `num_notes` > 0 when fewer than 25 have notes.
853. Pull via the contact → notes association and read the note body
86 (`hs_note_body`). One page of notes per lead, most recent first, is enough.
874. Scan the bodies for the urgency keywords and re-score with the +15 where a
88 keyword hits. Say in the caveats when notes existed but were not fetched
89 (beyond the cap) rather than claiming they contained nothing.
91---

Fetch note bodies for the urgency scan

The urgency keywords live in note bodies, and note bodies are engagements — they never arrive with the contact property pull. num_notes only says a note exists; the +15 keyword signal cannot fire unless the bodies are actually read.

How to do it without flooding the API:

  1. Score every lead first without the note-keyword signal (provisional score).
  2. Fetch associated notes only for the top ~25 leads by provisional score — or for every lead with num_notes > 0 when fewer than 25 have notes.
  3. Pull via the contact → notes association and read the note body (hs_note_body). One page of notes per lead, most recent first, is enough.
  4. Scan the bodies for the urgency keywords and re-score with the +15 where a keyword hits. Say in the caveats when notes existed but were not fetched (beyond the cap) rather than claiming they contained nothing.

Raw lines 74 to 92
93## Scoring model (0–100 composite)
95Four dimensions, each 0–25. Sum for composite.

Scoring model (0–100 composite)

Four dimensions, each 0–25. Sum for composite.

Raw lines 93 to 96
97### Engagement (0–25)
98Only count signals from the last 30 days. Older signals score 0.
100| Signal | Points |
101|---|---|
102| Email reply in last 14 days | +15 |
103| Email open in last 7 days (no reply) | +8 |
104| Site visit in last 7 days | +5 |
105| >3 outreach attempts, no reply | −5 |

Engagement (0–25)

Only count signals from the last 30 days. Older signals score 0.

SignalPoints
Email reply in last 14 days+15
Email open in last 7 days (no reply)+8
Site visit in last 7 days+5
>3 outreach attempts, no reply−5
Raw lines 97 to 106
107### Company fit (0–25)
108Default ICP if owner hasn't stated one: any industry, 1–50 employees.
110| Match | Points |
111|---|---|
112| Industry + size both match ICP | 25 |
113| Size matches, industry unknown | 15 |
114| Industry matches, size unknown | 12 |
115| Neither matches or both unknown | 5 |

Company fit (0–25)

Default ICP if owner hasn't stated one: any industry, 1–50 employees.

MatchPoints
Industry + size both match ICP25
Size matches, industry unknown15
Industry matches, size unknown12
Neither matches or both unknown5
Raw lines 107 to 116
117### Urgency (0–25)
119| Signal | Points |
120|---|---|
121| Note contains "urgent," "ASAP," "deadline," "budget approved" — requires the note-body fetch above; `num_notes` alone cannot fire this | +15 |
122| Lead age 7–21 days (prime follow-up window) | +10 |
123| Lead age <7 days | +5 |
124| Lead age >60 days | −5 |

Urgency (0–25)

SignalPoints
Note contains "urgent," "ASAP," "deadline," "budget approved" — requires the note-body fetch above; num_notes alone cannot fire this+15
Lead age 7–21 days (prime follow-up window)+10
Lead age <7 days+5
Lead age >60 days−5
Raw lines 117 to 125
126### Recency penalty (subtracted from composite)
128| Last activity | Subtract |
129|---|---|
130| <24 hours ago | −25 |
131| 1–3 days ago | −10 |
132| 4–7 days ago | −5 |
133| >7 days ago | 0 |
135---

Recency penalty (subtracted from composite)

Last activitySubtract
<24 hours ago−25
1–3 days ago−10
4–7 days ago−5
>7 days ago0

Raw lines 126 to 136
137## Custom ICP (runtime override)
139If the owner states an ICP at runtime ("focus on SaaS, 10–100 employees"), apply it for that session only.
141---

Custom ICP (runtime override)

If the owner states an ICP at runtime ("focus on SaaS, 10–100 employees"), apply it for that session only.


Raw lines 137 to 142
143## When the scores come out flat
145A CRM with no logged engagement scores every lead the same: ten leads, ten
146identical composites of 0. Sorting
147identical scores produces arbitrary order, and presenting that as a priority
148list is worse than saying nothing.
150**Check for this before showing a ranked list.** If the top and bottom composite
151differ by less than 10 points, the model has not separated anything. Say so
152plainly:
154> *"These leads score almost identically — your CRM has no logged emails, calls,
155> or site visits for them, so the engagement and fit signals are all empty. Here
156> they are ordered by what the CRM does know."*
158Then order by this ladder instead of by composite, and label it as such:
1601. `hs_lead_status` — `NEW` before `IN_PROGRESS` before empty
1612. `hs_is_unworked` = `true` — nobody has touched it yet
1623. `hs_buying_role` — contains `DECISION_MAKER`, then `CHAMPION`, then the rest
1634. `jobtitle` — seniority words (VP, Director, Head, Chief, Owner, Manager)
1645. `num_associated_deals` ≥ 1 — pipeline already attached
1656. `createdate` — newest first
167**Never present ladder order as a score.** Show it as "ordered by CRM signal"
168and drop the numeric score column entirely, so the owner is not reading
169precision that does not exist.

When the scores come out flat

A CRM with no logged engagement scores every lead the same: ten leads, ten identical composites of 0. Sorting identical scores produces arbitrary order, and presenting that as a priority list is worse than saying nothing.

Check for this before showing a ranked list. If the top and bottom composite differ by less than 10 points, the model has not separated anything. Say so plainly:

"These leads score almost identically — your CRM has no logged emails, calls, or site visits for them, so the engagement and fit signals are all empty. Here they are ordered by what the CRM does know."

Then order by this ladder instead of by composite, and label it as such:

  1. hs_lead_status — NEW before IN_PROGRESS before empty
  2. hs_is_unworked = true — nobody has touched it yet
  3. hs_buying_role — contains DECISION_MAKER, then CHAMPION, then the rest
  4. jobtitle — seniority words (VP, Director, Head, Chief, Owner, Manager)
  5. num_associated_deals ≥ 1 — pipeline already attached
  6. createdate — newest first

Never present ladder order as a score. Show it as "ordered by CRM signal" and drop the numeric score column entirely, so the owner is not reading precision that does not exist.

Raw lines 143 to 169