This spring I scored just under 20,000 active accounts for buying intent. Four of them reached a human’s review queue.
That ratio looks wrong at first. Nearly 20,000 accounts went in and four came out. It is easy to assume the thresholds were too tight or the funnel was being starved. I built the system, and I think four was the right answer. Each account had an evidence card built to be read in a minute, a reason to act now, and a suggested angle. About 700 more went to source review. Roughly 18,800 stayed on watch. About 420 were suppressed, with the reason recorded.
This is a build guide for an account intent model inside an operating loop. You can assemble a first version from a CRM export, one or two behavioral feeds, and a scripting language of your choice. Earlier pieces on this site argued that AI moves GTM’s bottleneck from execution to verification. This piece shows what that looks like in practice across the sources, gates, tables, and review loop.
The problem with intent signals
Good accounts often research before they talk to sales. They read docs. They inspect your GitHub. They visit the security page, then pricing, then security again. Three people from the same domain show up in the same week, and none of them fill out a form. By the time a form fill arrives, if it ever does, the evaluation may already be half over.
There are two common responses, and both are bad. The first is to wait for hand-raisers, which means arriving late to deals you did not source. The second is to chase every activity spike, which turns anonymous noise into sales tasks and teaches reps to ignore the word “intent.” In my experience, that takes about one quarter.
The better option is a system that finds possible buying motion early but refuses to spend anyone’s time without evidence, context, and a human decision. It produces three things in order.
- A routed queue an operator can review in one sitting.
- A readable evidence card for each account that might deserve action. It should explain why this account, why now, what they did, and what should happen next.
- A record of what work is allowed and what stays blocked until a human says otherwise.
Notice what is not on the list. Contacts, sequences, and sends come later. The system is account-first by design. A person can look like a good lead while the account context says no. Maybe there is an active opportunity someone else owns, a customer record, a competitor doing research, or a domain the CRM cannot resolve. Decide the account question first. Then the person question is cheaper and safer.
Here is one run of the system before we break it down.
Step 1: Start with a source catalog
The temptation is to start scoring on day one. Wait about a week. Build a source catalog first. Give every place that could tell you something about an account its own page, then answer three plain questions. What does this source prove? What can it not prove? How fresh is it?
My catalog covered the CRM (identity, ownership, account type, opportunity history, campaign context), a developer-intent feed (docs, web, and GitHub behavior tied to company domains), product analytics (a high-intent sample, not full coverage, which matters later), call recordings (metadata only), reply evidence from outbound systems, and a local database where all of it could be joined quickly.
That catalog should force four choices.
- The system of record for identity. The CRM owns who the account is, who owns it, and what has already happened commercially.
- The strongest behavioral source. The developer-intent feed shows evaluation-shaped activity before any form fill.
- The outcome target. Opportunity history lets you ask whether a real opportunity happened. If the model cannot eventually be tested against that, it is guesswork.
- The join layer. Use whatever lets you prototype fastest. Mine was a small database. Yours can be a warehouse or a spreadsheet.
The main design rule is to keep identity and intent separate. A CRM match proves the account is known and routable. It proves nothing about buying. A domain showing up in the intent feed proves activity happened. It proves nothing about who is behind it or whether they are safe to contact. The model carries both, shows them separately, and does not let one stand in for the other. Many bad intent systems fail right here.
Step 2: Score in the open
Someone will offer you a black box. It might be a vendor propensity score, a colleague’s model, or a chart that looks accurate but gives sellers nothing to inspect. Do not use that for version one. The first version of an intent score has two jobs. It needs to be directionally right, and it needs to survive a pipeline meeting. A normalized score like “70/100” is not enough on its own. “70/100 because developer activity, a platform-buyer path, a security evaluation, and a fresh useful reply all agree” is better, because every part of that sentence can be checked and challenged.
So the model is a visible scoring system. Each family gets its own column in the output.
| Family | The question it answers | Where it comes from | Cap |
|---|---|---|---|
| Fit | Could this company plausibly buy? | CRM firmographics, ICP segment membership, external research | Up to 10% of the raw basis before normalization |
| Behavior | Are they doing evaluation-shaped things? | Intent-feed events, docs and pricing and security activity, product usage | Up to 12% of the raw basis before normalization |
| Path | Does the evidence fit a recognizable motion? | Derived from fit, behavior, and reply evidence together | Strongest path counts; corroborating paths add a small amount |
| Timing | Is it recent enough to act on? | Last-activity dates against 14, 30, and 90 day windows | Small |
| Confidence | Do independent systems agree? | Count of unrelated sources showing the same account | Small |
| Identity | Do we know who this is? | CRM account and domain resolution | No score contribution. A gate. |
| Suppression | Should a guardrail override all of it? | Account type, opportunity status, viability rules | Subtracts; can zero the score |
The math is intentionally plain.
raw_points = fit + behavior + path_bonus + timing + confidence - suppression_penalty
intent_score = round(100 * max(0, raw_points) / score_basis)
The raw points are for inspection. The score people see is normalized to 100, because the route thresholds are easier to understand that way.
The important decisions are in the caps and exclusions. Fit is capped low because fit is a prior, not a signal. A perfect-fit account doing nothing is a mailing-list entry, not intent. Behavior is capped so one active domain cannot rise to the top on volume alone. Specific behavior (docs plus security plus pricing, multiple people) beats generic browsing. The path bonus takes the strongest single story and adds only a little for corroboration, so nine weak fragments do not beat one strong path.
Two exclusions matter more than any weight. CRM existence contributes nothing to confidence, because knowing who someone is tells you nothing about whether they are buying. Identity lives in its own column. Missing data is unknown, not negative. An account with no product-analytics coverage is not the same as an account with zero product interest. If the model cannot tell those apart, it will bury every segment your instrumentation missed.
Here is one account’s normalized score, the “70/100” from the sentence above, taken apart.
The rule here is simple. Claims need receipts. Your model should follow the same rule. If you cannot show the pieces of a score, the people receiving the queue will not trust it for long.
Step 3: Route, don’t rank
A ranked list tells people to start at the top. That instruction is often wrong. The top of an activity ranking can be full of cases you should not work, including customers doing support research, competitors reading your docs, an account with an open deal, or a university lab that will never buy.
So the score never goes straight to a seller. It produces a draft route. The route says what happens next, and each route names the thing that would move the account forward.
| Route | What it means | What unblocks it |
|---|---|---|
| Immediate review (A) | Strongest current evidence; a human inspects before anything happens | Human acceptance; then scoped enrichment may proceed |
| Passive review (B) | Worth a nurture lane, not worth credits yet | A human selecting the account by name |
| Source review | Interesting but unsafe because the evidence card, deal or call history, or identity still needs work | An agent-authored card, or a human read of the history |
| Watch | Plausible, not current | A named trigger, written down in advance |
| Active or recent deal | Context for the deal owner, not net-new outreach | Nothing. It routes to the owner, never the queue |
| Suppress | Out of the motion, on purpose, with the reason recorded | A human override with a name on it |
Thresholds map score bands to draft routes. In my build, on a 0 to 100 scale, 70 and up drafted immediate review, 45 drafted passive review, 20 drafted watch, and anything lower drafted suppression. Copy the transparency, not the numbers. Publish the bands next to the components so people can argue with both.
Then the structural gates get the final word in code. They run after the score and can overwrite it. Active opportunity? Route it to the deal owner regardless of score. Customer, former customer, or disqualified in the CRM? Suppress it for this motion. Domain that will not resolve to a CRM account? Send it to identity review, even if the feed looks hot. Competitor, university, agency, public sector? Suppress by default under a viability rule, unless a named human makes an exception.
This is what the distribution looks like when the gates are allowed to do their job.
The immediate-review bar is invisible at this scale. That is intentional. Immediate review should be rare. A high score means “look here first.” It does not mean “send now.” Every account that reaches a human costs minutes of judgment. The gates make sure those minutes go to the accounts with the strongest evidence.
Step 4: Treat evidence as structural
This is the first thing I got wrong.
Early versions of the model treated a recorded reply as positive intent. That sounds reasonable because somebody responded, and the field says so. Then you read a sample. The “reply” is an auto-acknowledgment. It is eight months old. It is “please remove me from this list.” It is a duplicate logged by two systems. It is a reply whose body the system never captured at all, which the flag does not tell you.
The fix is a two-layer rule. The structural layer records that something happened. A reply event exists, a call happened, or a form was submitted. The semantic layer records that someone, human or agent, read it and judged what it means. Only the semantic layer can add intent. The structural layer can create review work, but it cannot raise the score by itself. In my build, a fresh reply judged useful opens its own path in the score. An unread reply flag opens a task that says “someone needs to read this,” and the account stays in source review until that happens.
Call history gets the same treatment. Metadata (there were calls, this recently, about this deal) can route an account to “needs a transcript read.” It cannot mark the account warm. A transcript can say “we chose your competitor and signed for three years,” and no metadata field will catch that.
When I added the semantic layer, accounts that stale and duplicate replies had been promoting for weeks dropped back to research routes. Their activity did not change. The model changed. It stopped treating the existence of evidence as the meaning of evidence. That distinction is the difference between a queue people inspect and a queue people ignore.
Step 5: Wrap the model in a loop with memory
Everything so far describes a model. Run it once, get a queue. The difference between a model and a system is what happens on run two. If run two starts from scratch, you have a report generator. Every decision from last week is lost. This account is a competitor. That reply was stale. This one is waiting on a funding event. Then someone pays the same review cost again.
So the model runs inside a loop. The loop remembers and does not overwrite. Every run gets an ID. Every table is append-friendly. The core schema is small enough to sketch.
intent_runs -- one row per run: params, source freshness, counts
account_signal_snapshots -- one row per account per run: components, route
account_intent_routes -- current route per account, plus full history
account_evidence_cards -- why this account, why now, in rep language
account_research_actions -- which research stage each account needs next
account_suppressions -- the durable "no" list, with human overrides
operator_reviews -- accepts, rejects, reroutes, notes, selections
enrichment_actions -- approved spend only, with caps and approver
outcome_feedback -- meetings, opportunities, stalls, false positives
The minimum requirements are straightforward. Every table carries the run ID and the account ID. Snapshots carry each score component as its own column, plus the draft route and the final route, so “why did this change” is a query instead of an argument. Reviews carry reviewer, decision, and reason. Suppressions carry category, reason, and the name on the override. The join key is the normalized domain, everywhere, decided once and early. If you do not settle that up front, every table will disagree with every other table later.
Two mechanics are worth adding early. First, prior-route hydration means the loop looks up what route each account held last run before presenting the queue, then marks it newly flagged or recurring. Operators stop re-litigating accounts that merely persisted, and start noticing the ones that moved. Second, every run should report source freshness by showing which inputs refreshed and which are stale. A run built on a two-week-old CRM mirror may still be useful, but the label needs to say so.
One more rule is worth copying. The spreadsheet your operators review is a view, regenerated every run. The database is the record. If human edits live only in a sheet, you have a second system of record, and the two will diverge quickly.
Step 6: Plan the work, not just the score
A queue answers “who looks interesting.” The more useful output is a work plan. For each account that survived the gates, what is the next task, who owns it, and what is it blocked on? Give every stage a named owner from the start. A queue where “someone” will read the transcripts is a queue nobody reads. In my loop, every surviving account carries stage records across four kinds of work.
The evidence card. A card answers six questions in rep language. Why this account? Why now? What did they do? Why does it matter? What should happen next? Is paid enrichment allowed? Scripts assemble the raw material. The final card is authored judgment, written by an agent and kept until the evidence changes. Cards are reusable. Once written, future runs carry them forward instead of rebuilding the account story. Only a material change queues a refresh. An account with no card cannot leave source review, no matter its score. Raw activity counts do not go to sellers here; cards do.
External research. Useful, expensive, and easy to waste. The loop caps it (ten accounts per run, in my build) and enforces a thirty-day lookback. An account researched three weeks ago reuses that synthesis instead of spending another pass. Still-running research gets polled, not resubmitted. This keeps “researched” from becoming a loose label.
Call-history review. Queued as work for a person or agent when metadata says relevant calls exist, per step 4. The loop tracks needed, done, or not applicable. It never auto-interprets.
Paid enrichment. The recurring loop never enriches by itself. It records “blocked before human approval” and stops. Credits get spent only after a human accepts an A-route account or selects a B-route account by name. The approval becomes a row with an approver on it.
The pattern underneath all four is the same. Human judgment and paid data are limited. The loop’s job is to allocate those resources, not maximize a score. The score only decides who is worth planning work for.
Step 7: Make suppression durable
Suppression sounds like the boring part. It is one of the most useful parts. Every mature run of my loop checked a suppression register. That register held accounts placed out of the motion, each with a category, a reason, the run that first suppressed it, and space for a human override with a name attached.
Durability is the point. Without a register, the same false positives come back every month. The competitor’s engineers read your docs, the model lights up, and an operator spends the same five minutes confirming that this is still a competitor. With a register, that conclusion is recorded once. The account stays suppressed across future runs until a named human decides otherwise. Activity does not reactivate it. Judgment does.
The register also holds the category-level defaults from step 3, including competitors and adjacent vendors, education, agencies and consultancies, public sector, and businesses that will never run your kind of evaluation. That means the viability decision happens once, in a written rule, instead of account by account in a queue. In the pilot, about 420 accounts landed there. By early summer, the durable register carried 27 standing suppressions. Each one was a repeated review task the team no longer had to do.
A suppression list gets more useful with age if only a human can shrink it. It is the team’s judgment about who not to pursue, written where the automation has to respect it.
Step 8: Close the loop with outcomes, and tune conservatively
The last two tables in the schema, operator_reviews and outcome_feedback, are where the system starts to learn. Every human decision about a routed account gets recorded, including accepted, rejected, rerouted, and selected for enrichment. The notes matter too. Every result gets recorded as well, whether the account booked a meeting, created an opportunity, stalled, or turned out to be a false positive.
After a quarter or so, that gives you the dataset that matters for tuning. The question is not “which accounts scored high.” The question is “which kinds of evidence, after scoring high, did humans accept, and which of those turned into real pipeline.” The tuning moves should be conservative.
- Signal combinations that humans accept and that later produce meetings or opportunities earn more weight, slowly.
- Combinations humans keep rejecting earn a gate or a penalty, faster.
- Explanations that keep appearing in review notes (“this is an agency,” “this activity is a support pattern”) get promoted into structured features or suppression categories, so the insight fires automatically next time.
- Weights drift. Gates do not. No backtest result, however strong, auto-removes a human review step.
Why so conservative? Because the failure modes are not equal. A slightly miscalibrated weight costs you ordering. A silently removed gate can cost you a seller’s trust, or create an embarrassing outreach to an account with an active deal. You may not find out for weeks. Tune reversible things freely. Change trust-bearing gates slowly.
The version you can build this month
Everything above describes the full build. The minimum is smaller.
- A CRM export and one behavioral source, joined on domain.
- A weekly script that scores in visible families and assigns draft routes.
- A review sheet with the route, the components, and the prior route per account.
- A suppression tab that the script reads back in and respects, every single run.
- A log where accepts, rejects, and outcomes get one row each.
That is a loop. It remembers, refuses, and learns at the speed of your logging. The nine-table version is that spreadsheet with better structure. You will know you need it when the sheet’s history tabs become hard to reconcile. At any scale, you still need append-only history, the semantic layer from step 4, and the rule that money and outreach wait for a named human.
What one run actually looks like
By early summer the loop was routine. The counts below are from one June cycle. It was a smaller recurring run over accounts already in play, not the spring pilot from the opening. The table shows what the loop actually did.
| One run, June 2026 | Count |
|---|---|
| Accounts reviewed | 50 |
| Immediate review (A) | 4 |
| Passive review (B) | 5 |
| Source review | 39 |
| Watch | 1 |
| Suppressed | 1 |
| Evidence cards reused from prior runs | 21 |
| Evidence cards still needed | 29 |
| External research runs executed | 6 |
| External research skipped, still fresh | 42 |
| Enrichment actions blocked pending approval | 48 |
| Standing suppressions in force | 27 |
Source: one production run of the author’s loop, June 2026. Counts exact.
The bottom half matters most. Twenty-one account stories carried forward instead of being rewritten. Forty-two research passes were skipped because the loop remembered they already happened. Forty-eight enrichment actions were blocked until a person approved them. The top half is a queue. The bottom half is a system doing less repeated work without giving up judgment.
Small changes compound
Here is a simple example of why small loop improvements matter over the long term.
Take a motion that reviews 40 routed accounts a month. Sales accepts 60 percent of them, 30 percent of accepted accounts take a meeting, half of meetings become opportunities, and ACV is $60k.
40 reviewed x 60% accepted x 30% meeting x 50% opp x $60k ACV
= 3.6 opportunities and roughly $216k of new pipeline per month
Now let the loop do what steps 7 and 8 are for. Every month, a few false positives become suppression rules and a few review notes become features. Acceptance rate and meeting rate each improve by a small relative amount month over month. Use one, two, or three percent, depending on how consistently the feedback tables get used. Nothing else changes. Same headcount, same account flow, same ACV.
Two percent looks small in a single month. Over a year, it changes the baseline. Month 12 produces $334k instead of $216k, a 55 percent higher run rate at the same capacity.
By year end, the two percent line is $661k of pipeline ahead. At a 25 percent win rate, that is about $165k of extra closed revenue from the same operating capacity. That is the point of the loop. Small decisions now, logged consistently, become a larger operating advantage later.
Build credibility before action
The point of all this gating is not caution for its own sake. It is that the four accounts that reached a human were credible. They had current evidence, a readable story, clean account context, no active deal, no competitor in disguise, and enrichment waiting for a named human’s yes. When the queue is that defensible every week, the people receiving the work can take “the model flagged it” seriously.
Very little of the hard work here was modeling. The hard work was setting limits around when a signal can take someone’s time, spend money, risk trust, and who has to approve it. The model finds where to look. The loop decides what work happens next. Humans decide what ships. Outcomes tune the next pass. Build all four layers and you get an intent process that improves every run. It can catch early buying motion without treating a signal as permission to act.