Growth & Marketing AI
A Dynamic ICP Model That Catches Its Own Drift
Result:
A live ICP model that re-weights each segment against real conversion and revenue data, catching drift like a dip in SME-segment conversion early enough to redirect spend and content before it shows up in the topline number.
Part of a series: this recipe is the natural next step once readers are identified in PostHog, so there’s a real person, not an anonymous session, for it to score. It also stands on its own if you already have accounts, conversion, and revenue data from anywhere.
▲ PATTERN DIAGRAM
ICP (noun) — an ideal customer profile, treated as data with a feedback loop instead of a static CRM tag: authored once, scored continuously, and checked against who’s actually winning.
BEFORE · STATIC TAG
Brittle, and rarely checked against reality#
A CRM field like hs_ideal_customer_profile drives targeting, content, and spend for a
year — with no path back to conversion or revenue. Drift is invisible until pipeline
comes in soft.
NOW · DYNAMIC MODEL
Data with a feedback loop#
The hypothesis is a row, scored 0–1 against every account, blended with live conversion and revenue, and ranked against segments nobody tagged. When it disagrees with reality, the definition moves.
1 · AUTHOR
Store the hypothesis as a row#
An icp table: segment, size band, industry, region — one row per tier.
2 · SCORE
Score every account 0–1#
icp_fit_score joins accounts to icp, recalculating as behavior changes.
3 · BLEND
Weight live performance in#
A weighted moving average of conversion + revenue, recent days weighted heavier than the original assumption.
Details →4 · SURFACE
Show the gap to reality#
Percent converting, percent generating revenue, revenue itself — hourly and daily via pg_cron.
Details →5 · RANK
Rank segments, no tiers#
Same data, cut without regard to ICP tier, surfaces whoever’s winning right now.
Details →Shortcut
Paste this into Claude Code with access to your database migrations once the tables below exist.
Requires manual input
A working connection to your own Postgres database, with the three tables in “Data requirements” below already populated (accounts, conversion events, revenue). The connection string has to be yours; there’s nothing to hand over blind.
Build a dynamic ICP model against my existing accounts, conversion_events,
and revenue tables in Postgres:
1. Write a migration for an `icp` table storing authored ICP hypotheses:
segment name, size band (min/max employees), industry, region, one row
per tier.
2. Write a view `icp_fit_score` that joins accounts to icp and scores each
account 0-1 against every ICP row's criteria.
3. Add a materialized view that blends icp_fit_score with a weighted
moving average of conversion_events and revenue per account, weighting
the last N days more heavily than older data.
4. Set up two pg_cron jobs against that materialized view: hourly for the
conversion numbers, daily for revenue.
5. Write a query that ranks accounts by conversion and revenue alone, no
ICP tier, and diffs it against the authored tiers to surface accounts
winning without being tagged tier 1.
Use my actual table and column names; ask me for them if you can't see the
schema.Problem#
Most ICP definitions are authored once and trusted forever. Someone writes down “tier 1 =
mid-market, 200–2000 employees, EMEA” (HubSpot even ships a field for it,
hs_ideal_customer_profile), and that tag drives targeting, content, and spend for the
next year regardless of whether accounts matching it convert or pay. An ICP is a
hypothesis, not a fact. Nothing in a typical CRM checks it against outcomes, so drift is
invisible until a quarter’s pipeline comes in soft and nobody can say which segment caused it.
Pattern#
Treat the ICP as data with a feedback loop:
- Store the hypothesis as a row. An
icptable holds the authored criteria (segment, size band, industry, region) as structured, queryable data. - Score every account against it. A derived view scores each account 0–1 against each ICP’s criteria, turning “does this account fit tier 1” into a number that recalculates continuously as behavior changes.
- Blend the score with what’s happening now. A weighted moving average pulls in live conversion and revenue data for the accounts matching each ICP, so recent performance counts for more than the original assumption. A segment that converted well a year ago but is slipping now gets re-weighted down even as its label stays the same.
- Surface the gap between the score and reality. For each authored ICP, show percent converting, percent generating revenue, and revenue itself, refreshed on a schedule (funnel-sensitive numbers hourly, revenue daily) so the “actual” side never goes stale.
- Rank the emergent segments too, independent of what was authored. The same conversion and revenue data, cut without regard to ICP tier, surfaces whichever accounts are winning right now, even ones nobody tagged as tier 1. When that ranking disagrees with the authored tiers, the definition is wrong and should move.
This is also why the model couldn’t live as a HubSpot workflow: hs_ideal_customer_profile is a
static text field with no path back to conversion or revenue. Building it properly meant making
ICP fitment a first-class part of the CRM’s own data model, one of the reasons the CRM itself
got replaced with a homegrown, event-sourced system built for that requirement.
Data requirements#
Before the pattern above produces anything real, three tables (or equivalent warehouse views) need to already exist and be populated:
- Accounts: one row per company, with the firmographic fields the ICP hypothesis gets scored against (employee count, industry, region).
- Conversion events: timestamped, tied to an account, at whatever stage the weighting cares about (opportunity created, closed-won, trial started).
- Revenue: timestamped and tied to an account, not a lifetime total. The weighting needs freshness, so this has to be a series over time, not a single running number.
If any of the three lives in a system that isn’t queryable alongside the others, the join is the real blocker. Land the missing table before touching the scoring logic.
Example prompts#
Feed these to Claude Code once the tables above exist, one per step of the pattern:
- “Write a Postgres migration for an
icptable storing authored ICP hypotheses: segment name, size band (min/max employees), industry, region, one row per tier.” - “Write a view
icp_fit_scorethat joinsaccountstoicpand scores each account 0–1 against every ICP row’s criteria.” - “Add a materialized view that blends
icp_fit_scorewith a weighted moving average ofconversion_eventsandrevenueper account, weighting the last N days more heavily than older data.” - “Set up two pg_cron jobs against that materialized view: hourly for the conversion numbers, daily for revenue.”
- “Write a query that ranks accounts by conversion and revenue alone, no ICP tier, and diffs it against the authored tiers to surface accounts winning without being tagged tier 1.”
Swap in your own table and column names. The shape of each prompt carries over even when the schema underneath doesn’t.
Output#
Run the same loop against your own tiers and a segment quietly underperforming its label surfaces within weeks, in time to act before the quarter closes — not a quarter later, once the topline number already shows it.
Variations#
- The match threshold for “does this account count toward this ICP’s score” is a tuning knob: tighter thresholds cut false positives but shrink the sample the scorecard is built on.
- Swap in whatever conversion and revenue tables your own CRM already has: this is a re-weighting loop that works on top of any CRM, including HubSpot’s own objects, just without the ability to make the ICP field itself derived.
- The same shape catches churn early: filter the scorecard to one segment and alert on week-over-week conversion drop, weeks before a quarterly review would surface it.
- Next in this line. The same feedback loop applies one level down, to individual buyer personas, and the whole model becomes queryable by a non-engineer once it sits behind a roster-intelligence MCP server.
Tools used
- PostgreSQL
Free · open source
- pg_cron
Free · open source