GTM Engineering
Roughly 90% of Lead Scores Still Route on a Job Title
A VP title and a company size predict almost nothing about who buys. What buys is behavior: product usage and intent. Here is the scoring rubric that weights signals, the SQL that computes it, and the routing rule that acts on it.
· 13 min read
Roughly 90% of the lead-scoring models I have opened route on a job title and a company size. Points for “VP or above,” points for “500 or more employees,” points for the right industry picklist, and a threshold that hands the total to a rep. The model has never seen the one thing that predicts a purchase: what the person did. A director who opened three docs, invited two teammates, and hit the pricing page twice this week scores lower than a VP at a big logo who has done nothing but exist in the right firmographic band.
That is the whole failure. Demographics tell you who a person is. Behavior tells you what they are about to do. Scoring on the first and ignoring the second is why sales rejects half the “hot” leads you send and why the ones they never call turn out to be the ones who bought from a competitor. I have rebuilt this on enough stacks to know the fix is not a better title-points table. It is a second axis, computed from product and engagement signals, and a routing rule that reads it.
The fix is not a longer title-points table. It is a second scoring axis, and it goes in in a fixed order: fit becomes a gate, behavior gets instrumented, intent is computed on a rolling window, the whole thing is validated against closed-won, and only then does routing read the pair. Skip a rung and the one above it sits on sand. Here is the ladder, bottom to top.
- L5Route on the pair and enforce the SLAunder 1hr
High fit plus hot intent routes to an AE with a sub-hour SLA and the last five events attached. Low fit plus hot intent goes to self-serve. Everything else nurtures. The score is inert until this rule reads it. This is the top rung, not the first.
- L4Validate against closed-won before you route2 quarters
Run the intent model as if it existed two quarters ago and confirm your actual closed deals scored hot in the weeks before they signed. Check the distribution: 90% in one band is a label, not a score. If the wins did not score hot, the weights are wrong.
- L3Compute intent on a rolling 7-day window7-day
Roll those events into a weighted number that resets, so it reflects the last week, not a lifetime total. Recent, stacked signals score highest; the last 7 days convert about 3x older ones. Count each signal once per window.
- L2Instrument the five behaviors that predict a buy5 events
Get product and web events into the warehouse: pricing views, core-value action, teammate invites, doc opens, return sessions. You do not need every event, you need the five that correlate with closed-won. Pull them from the last two quarters of wins.
- L1Fit as a gate, not a points pileA / B / C
Collapse title, company size, industry, and region into a letter grade that qualifies an account into the pool. Stop letting a VP title out-score a director who is three doc-opens deep. Fit answers whether to sell to the account, nothing more.
Fit is who they are. Intent is what they do.
Two axes, and the mistake is blending them into one number. Fit is firmographic: title, company size, industry, region, tech stack. It answers “should we sell to this account at all.” Intent is behavioral: product usage, doc opens, pricing-page visits, teammate invites, repeat sessions. It answers “is this person moving toward a decision right now.” A single blended score that decays on engagement is a confused composite, because a high-fit account with zero intent and a low-fit account with screaming intent can land on the identical number and get treated the same way. They are not the same lead.
The reason the industry defaulted to fit-only is that fit was the data you could buy. Firmographics come from an enrichment provider on day one. Behavior requires instrumentation: product events flowing to a warehouse, engagement events captured from the site and the app. That plumbing is more work than an enrichment append, so most teams stopped at the append and called it scoring. The problem is that fit alone does not move. A VP is a VP whether they are three weeks from signing or have never heard of you.
The gap between the two axes shows up in reply rates, which is the cleanest proxy for whether a score is pointing reps at the right people. Outreach driven by firmographic lists behaves like spray; outreach driven by a fresh behavioral signal behaves like a warm intro.
| How the lead was selected | Reply rate | What the score was reading |
|---|---|---|
| Spray to a firmographic list | 1-3% | Title and company size only |
| Segmented firmographic bulk | 3-7% | Better firmographics, still static |
| Signal-triggered (behavior) | 10-20% | A recent product or intent event |
| Fully personalized to context | 20-40% | Behavior plus account research |
Source: Woodpecker’s analysis of 26,000 campaigns. The jump from the top row to the third is the entire argument for a behavior axis: same reps, same product, a 3x to 10x lift in reply rate purely from selecting on what people did instead of who they are.
The rubric: weight behavior, cap firmographics
Here is the rubric I build. Fit is a letter grade from firmographics and caps at a gate, not a pile of points. Intent is a number, and it is where the weight lives, because intent is what changes and what predicts the next 30 days. The scores stay separate. Routing reads the pair.
| Signal | Type | Weight | Why it earns the weight |
|---|---|---|---|
| Pricing page visited 2+ times in 7 days | Intent | 25 | Highest-correlation pre-buy behavior; near-term |
| Product: invited a teammate | Intent | 20 | Expansion of use, buying-committee formation |
| Product: hit a core-value action | Intent | 20 | Reached the moment that predicts retention |
| Opened 2+ docs / demo pages this week | Intent | 15 | Active evaluation, recent |
| Return session within 72 hours | Intent | 10 | Signal freshness; last 7 days convert ~3x older |
| Title is buyer or above | Fit | gate | Qualifies the account; does not stack points |
| Company in ICP size and industry | Fit | gate | Qualifies the account; does not stack points |
Source on the weighting logic: signal freshness and strength tiering follow the same signal-based outbound practice that ranks recent, stacked behavioral signals far above single or stale ones, and the Woodpecker 26,000-campaign benchmark showing signal-triggered outreach at 10-20% reply against 1-3% for firmographic spray. Fit is deliberately a gate. A VP title should qualify an account into the pool; it should not out-score a director who is three doc-opens and a pricing visit deep, because the director is the one moving.
The SQL that computes it
Behavior scoring lives where the behavior lives: the warehouse, next to product events and web events, not in a CRM field a marketer types into. Here is the model that rolls raw events into an intent score per person, windowed to the last seven days so it reflects what is happening now, not a lifetime accumulation that never resets.
-- intent_score: behavioral, windowed, freshness-weighted
-- runs on the events already flowing to the warehouse, syncs back to CRM
with events as (
select
person_id,
event_name,
occurred_at
from analytics.gtm_events
where occurred_at >= current_date - interval '7 days'
),
scored as (
select
person_id,
-- weight each behavior; a signal counted once per window, not per fire
max(case when event_name = 'pricing_view' then 25 else 0 end)
+ max(case when event_name = 'teammate_invited' then 20 else 0 end)
+ max(case when event_name = 'core_value_action' then 20 else 0 end)
+ least(count(distinct case when event_name = 'doc_open' then occurred_at end) * 8, 15)
+ max(case when event_name = 'return_session' then 10 else 0 end)
as intent_raw
from events
group by person_id
)
select
person_id,
intent_raw,
case
when intent_raw >= 55 then 'hot'
when intent_raw >= 30 then 'warm'
else 'cool'
end as intent_band
from scored;
The max() per behavior is deliberate: it counts a signal once inside the window instead of rewarding someone for reloading the pricing page ten times, which is noise, not ten times the intent. The doc-open line uses a capped count, so evaluation depth adds up but does not run away. Fit stays in its own model, a letter grade off firmographics, and the two join at routing time.
-- join fit + intent at routing time; never sum them into one column
select
p.person_id,
f.fit_grade, -- A / B / C from firmographics
i.intent_band, -- hot / warm / cool from behavior
case
when f.fit_grade in ('A','B') and i.intent_band = 'hot' then 'route_now'
when f.fit_grade in ('A','B') and i.intent_band = 'warm' then 'route_sla_1d'
when f.fit_grade = 'C' and i.intent_band = 'hot' then 'plg_selfserve'
else 'nurture'
end as routing_action
from people p
join fit_scores f on f.person_id = p.person_id
join intent_scores i on i.person_id = p.person_id;
The routing rule that acts on it
A score sitting in a warehouse column is not a signal until a workflow reads it. The routing rule is the other half of the build, and it has to respect the one number the whole exercise is chasing: speed. RevenueHero’s 2025 audit found 63.5% of 1,000 B2B sites never responded to an inbound lead at all, and those that did averaged 29 hours. Signal-triggered outreach only earns its 10-20% reply rate if it fires while the signal is warm, so a hot lead that routes correctly but sits in a queue for a day has been scored well and routed uselessly.
# routing rule: reads the pair, enforces the SLA, degrades safely
on: intent_score.updated
match:
fit_grade: [A, B]
intent_band: hot
then:
assign: round_robin(pool = "AE_ICP")
sla_minutes: 60 # under 1 hour or it escalates
on_breach: notify(manager) + reassign(next_available)
notify_rep: slack(context = last_5_events) # the signal, not just a name
else_if:
fit_grade: C
intent_band: hot
then:
route: plg_selfserve # high intent, low fit: do not burn an AE
watch: expansion_signal
default:
route: nurture # no rep touch until intent moves
The rule sends the rep the last five behavioral events, not a bare name and a score. That is the difference between “here is a hot lead” and “here is a director who invited two teammates and hit pricing twice on Tuesday,” which is a call the rep can open with. It also degrades safely: a low-fit high-intent lead goes to self-serve instead of burning an AE hour, and an unbreached hot lead escalates rather than rotting in a queue. This is the routing half of the scoring and routing discipline; the scoring half is the rubric above, and neither works without the other.
What changes when you flip the axis
Same 1,000 leads, scored two ways. The demographic-only model marks a big band “hot” on title and size and hands them to reps who reject most of them. The behavioral model marks a smaller, denser band hot on what people did, and the reps convert it because the leads are moving. Toggle between them.
View as table
| Item | Value |
|---|---|
| Hot (routed to AE) | 420 |
| Warm | 260 |
| Nurture | 320 |
| PLG self-serve | 0 |
L4 is the rung teams skip
Run the ladder in order and one rung does the quiet work of saving you from a confident, wrong model: validation against closed-won. Teams instrument the events, compute a clean-looking intent score, and wire routing on top of it without ever checking that the score would have caught the deals they already won. A score nobody validated against real outcomes is a guess with decimal places.
The check is an afternoon. Pull your last two quarters of closed-won, run the intent model as if it existed back then, and confirm the deals that closed scored hot in the weeks before they signed. Two failure shapes show up. If the wins did not score hot, the weights point at the wrong behaviors and no routing logic will rescue a model that cannot see its own buyers. If 90% of every lead lands in one band, the thresholds are labels, not a score, and you recalibrate before a rep sees a number. Both are cheap to catch here and expensive to catch after routing runs on them for a quarter.
Demographic-only versus behavior-weighted
| Demographic-only scoring | Behavior-weighted scoring | |
|---|---|---|
| What it measures | Who the person is (static) | Who they are and what they are doing now |
| A VP who has done nothing | Scores hot, routed to a rep | High fit, zero intent, nurtured |
| A director evaluating hard | Under threshold, ignored | Hot intent, routed with context |
| Low-fit high-intent user | Buried in nurture | Self-serve, watched for expansion |
| What the rep receives | A name and a number | A name and the last five behaviors |
| Rep trust in the score | Rejects the hot band | Works it, because it is moving |
The through-line: a title is a fact about the past and behavior is evidence about the next 30 days, and you want to point the scarce rep hour at the second one. Stop scoring on who showed up in the enrichment append. Score on who is moving, compute it from the events you already have flowing, and let the routing rule fire while the signal is still warm.
Pull your last two quarters of closed-won this week and check one thing: did those deals score hot on your current model in the weeks before they closed? If the answer is no, the model is scoring the wrong axis, and the rubric and the SQL above are the afternoon that fixes it.
Keep reading
One email. Every week.
One email a week: a system I built or broke, with the config, the numbers, and what I would change. No roundups, no theory, unsubscribe whenever it stops being useful.
The newsletter opens soon.
Connect a provider in src/config.ts