Skip to content
Playbook

How to build a targeted B2B lead list (ICP, sources, and hygiene)

A list is a matching problem. You can only target on fields you hold — so write the ICP in fields, and know what your filter does with a missing one.

4 May 2026 12 min readBy Autocloz Editorial, GTM team
How to build a targeted B2B lead list (ICP, sources, and hygiene)

Building a targeted list is a matching problem, and the constraint is blunt: you can only target on fields you actually hold. That single fact reorders the whole task. Write the ICP as field conditions rather than adjectives, learn what your filter does with a value that is missing, verify before you send rather than after, and deduplicate on identity rather than on strings. Do those four things and the list is targeted. Skip any of them and you have a large spreadsheet that will look fine until the bounces arrive.

An ICP written in adjectives cannot be matched

Most ideal customer profiles are written as descriptions. "Fast-growing mid-market B2B companies with modern sales teams" reads well in a deck and is unusable as a filter, because no column in any database contains "fast-growing" or "modern".

The rewrite is mechanical. Take each adjective and ask what observable field would evidence it, then replace the adjective with that field's condition.

  • "Fast-growing" becomes headcount between 50 and 250, or a job-posting count above some threshold, or a funding event in the last two quarters.
  • "Mid-market" becomes an explicit headcount or revenue band with both ends named.
  • "Modern sales team" becomes a job title that exists — a head of revenue operations, a sales enablement lead — or a technology signal.
  • "B2B" becomes an industry value from whatever taxonomy your source uses.

The test. Hand the profile and a spreadsheet to somebody who does not work on your team and ask them to sort every row into in or out. If they can do it without asking you a question, the profile is a specification. If they cannot, it is a description, and no tool will save it. The strategic half of this — how to choose the segment in the first place rather than how to express it — is covered in building an ideal customer profile, and the ICP generator produces a first draft you can then rewrite into fields.

The five fields a filter can actually act on

Nearly every B2B targeting system reduces to the same small set, because these are the fields that are reliably populated across sources. Autocloz's autopilot uses exactly five, and knowing them changes how you write the profile.

  • Title. The person's job title, matched against a list of titles you supply.
  • Industry. The company's industry, matched against a list of values.
  • Location. A country, matched against either the person's country or the company's.
  • Headcount minimum and headcount maximum. A company-size band.

That is the whole surface. Everything else you might want to target on — technology stack, funding stage, growth rate — has to arrive as an enrichment that lands in one of those fields or as a tag you apply yourself.

Two behaviours in that list surprise people, and both are worth designing around rather than discovering later.

Industry is an exact string comparison. "SaaS" and "saas" are different values. If your source writes "Computer Software" and your profile says "Software", the filter matches nothing and reports zero rather than reporting a mismatch. Always check the actual distinct values in your data before writing the industry condition, not the values you assume are there.

Location is matched at country granularity. A profile targeting a city has to express that as a country plus a manual pass, or as a tag applied during import.

Why substring matching on job titles builds the wrong list

This is the failure that quietly wrecks more lists than any other, and it has two directions.

Over-matching. A naive filter tests whether the title *contains* your keyword as a substring. A search for "hr" is then satisfied by "Threading Systems Director", because the letters are present in that order. Autocloz hit exactly this: an ICP of "HR Director" enrolled a "Threading Systems Director", which is funny once and expensive at scale. The fix is word-boundary matching — in Postgres, a regular-expression match anchored with \m and \M around each token rather than an ILIKE '%hr%' — so the token has to be a whole word.

Under-matching. The mirror failure. A filter for "CHRO" does not match "Chief Human Resources Officer", because they share no tokens at all. Word boundaries do not help here; an alias table does. Any serious title filter needs an expansion layer that knows CHRO, CPO, VP People and Head of HR can name the same job.

How to diagnose which one you have. Pull thirty matched rows at random and read the titles by hand. Over-matching shows up immediately as titles that are obviously wrong. Under-matching is invisible in matched rows, so you have to check the other side: take thirty rows the filter *rejected* and see whether any of them should have qualified. Almost nobody does the second check, which is why under-matching survives for months.

One more behaviour to know: a row with no title recorded is excluded by a title filter rather than included. That is the strict and correct default for outbound — a prospect whose job is unknown should not be swept into a campaign built on job titles — but it means an enrichment gap looks identical to a mismatch in your counts.

What a missing field does to your filter

Null handling is the most consequential detail in list building and the least discussed. A filter has to decide what a missing value means, and there are only two defensible answers: exclude it, or include it.

Autocloz's headcount filter includes it. The condition for a minimum headcount is, in effect, "the company's maximum size is unknown OR is at least the minimum you asked for". A company with no headcount recorded therefore passes a headcount filter rather than failing it. The same is true of the maximum.

Whether that is right depends on your source. If your enrichment covers headcount for 90% of rows, including the unknown 10% is generous and harmless. If it covers 40%, your "50 to 250 employees" list is more than half companies of unknown size, and the profile is not doing the work you think it is.

The rule. Before you trust any filter, count how many rows have a value in each field you are filtering on. A one-line count of nulls per column tells you more about your list than any dashboard. Then decide, per field, whether unknown should pass or fail — and if your tool does not let you choose, know which way it goes.

A related trap. Counting matches through the filter and counting enrolments after it can legitimately differ, because a strict second pass may drop rows the fast SQL pass over-collected. If your preview says 1,200 and your campaign enrols 1,150, that is not necessarily a bug. Ask what the difference is made of before assuming either number is wrong.

Verification tells you what an address is not, and rarely what it is

Verification is sold as a yes/no answer. It is not, and understanding the actual verdicts changes how you use them.

A local verification pipeline runs a series of layers and stops at the first definitive answer:

  • Syntax. Malformed addresses fail here.
  • Typo detection. Edit-distance matching against the most common domains catches gmial.com and hotnail.com.
  • Disposable domains. Throwaway providers, matched against a maintained list of a few thousand.
  • Role addresses. info@, support@, admin@. Flagged as risky, never blocked, because role accounts damage reputation rather than delivery.
  • DNS and MX. A domain with no mail exchanger cannot receive mail, so this is a definite undeliverable.

Everything after that is where honesty matters. Autocloz's built-in verifier opens no SMTP connection at all, which means it cannot return a deliverable verdict on its own — an address that clears every layer is reported as risky and unconfirmable. That is the correct answer rather than a limitation to work around. RFC 5321 section 3.5.3 states that a server "MUST NOT return a 250 code in response to a VRFY or EXPN command unless it has actually verified the address" and provides reply code 252 for the cannot-confirm case, and many servers disable those commands entirely. A catch-all domain accepts every recipient at the protocol level, so a probe against one proves nothing. Deliverable is written only when a real delivery is accepted.

How to act on each verdict:

  • Undeliverable. Remove it. There is no send policy that makes this row worth having.
  • Risky. Segment it. Send at lower volume from a well-established mailbox and watch bounces closely. Do not mix it into your main list and do not delete it.
  • Unknown. Retry later. A network timeout is not evidence about the address.
  • Deliverable. Send normally, and remember the verdict has a shelf life.

That shelf life is the part people skip. Autocloz treats a stored verdict as fresh for 30 days and re-verifies past that, which is a reasonable default because addresses go stale at the rate people change jobs. The GDPR's accuracy principle at Article 5(1)(d) requires personal data to be "accurate and, where necessary, kept up to date", so in Europe the hygiene cycle is an obligation rather than an optimisation. The mechanics of the cleaning pass are in how to clean your email list, and a single address can be checked against the same layers with the free email checker.

Autocloz's free plan covers 5 users and 10 mailboxes and includes 1,000 email verifications a day, rising to 10,000 on Growth, 50,000 on Pro and 250,000 on Scale — start free and verify a real list before you commit to a source.

Deduplication is an identity problem, not a string problem

Two sources will hand you the same person, and the duplicate will not be an exact string match. Deciding what makes two rows the same human is the whole of deduplication.

The candidate keys, with what each one gets wrong:

  • Email address. The strongest signal and the one that under-merges. [email protected] and [email protected] are the same person and will not merge.
  • Normalised name plus company domain. Merges the case above and over-merges elsewhere — two people genuinely share a name more often than intuition suggests, and every row with a blank domain collides with every other blank. Autocloz's cross-source deduplication uses exactly this key, hashing the normalised full name and domain together, which is why it should be treated as a suggestion rather than an automatic merge.
  • LinkedIn profile URL. Excellent when present, absent most of the time.

The practical rule. Merge automatically on exact email. Merge on name-plus-domain only into a review queue that a person confirms. And when two rows do merge, decide field by field which value survives rather than letting the merge take whichever row arrived first — the merged record inherits the worse email as often as the better one, and every downstream bounce starts there.

A worked funnel from 10,000 rows to a sendable list

Numbers here are illustrative and exist to show the shape of the attrition, not to predict yours. Run this on your own data and the percentages will differ; the sequence will not.

  1. Raw import: 10,000 rows from two sources.
  2. Deduplicate. Overlap between two sources in the same segment is typically substantial. Say 1,400 duplicates merge. 8,600 remain.
  3. Apply the title filter with word boundaries. Most databases hold many titles per company and you want one or two. Say 62% drop out. 3,270 remain.
  4. Apply industry and headcount. Some rows have no industry and some have no headcount; you decided above whether those pass. Say 18% drop. 2,680 remain.
  5. Verify. Say 6% undeliverable and 14% risky. 2,520 sendable, of which 375 are the risky segment.
  6. Suppress. Existing customers, open opportunities, previous unsubscribes, anyone contacted in the last 90 days. Say 9%. 2,290 remain.

Two observations from that shape. Roughly three-quarters of a raw import does not survive to a first send, and every one of those removals was cheaper than the bounce it prevented. And the biggest single drop is the title filter, which is exactly the step most likely to be wrong — which is why sampling thirty matched titles by hand is the highest-value ten minutes in the whole process.

Segment so the message stays true for every row

A list is finished when every row can receive the same message and have it be true. If a sentence in your email is true for half the list, you have two lists.

Segment on the axis that changes the message, and only that axis. Company size changes the pain. Industry changes the vocabulary. Role changes what the person is accountable for and therefore what they care about. Location changes the timing, the currency and sometimes the legal basis. Everything else is usually a distinction that does not earn a separate variant.

A useful sanity check: if two segments would receive messages that differ only by a merge field, they are one segment. If they would receive genuinely different arguments, they are two, and each needs its own list rather than a shared list with conditional copy.

Once segments exist, ordering within them is a scoring problem rather than a filtering one. A rule engine that adds points for title seniority, industry match, company size band, country, tags, recent activity and verification confidence — summed and capped — gives you a priority order without changing who is in or out. The distinction between filtering and scoring is set out in what lead scoring is, and for a comparison of building lists in-workspace against renting an enrichment layer, the Autocloz and Clay comparison covers both models.

What list building in Autocloz does not do

Four limits worth knowing before you build a process around any of this.

It does not decide whether unknown should pass or fail. The headcount filter includes rows with a missing value and the title filter excludes them, and those defaults are fixed rather than configurable per campaign. If your enrichment coverage is thin, that asymmetry is doing real work in your counts.

It does not confirm a mailbox exists. The built-in verifier deliberately opens no SMTP connection, so its best verdict for an address that clears every layer is risky and unconfirmable. Paid verification providers can be connected for a stronger verdict; the free path is honest about what it can prove.

It does not match on anything beyond title, industry, location and headcount at the autopilot layer. Technology, funding stage and intent have to arrive as tags or as enriched values in those fields.

It does not tell you whether the list is *good*. Analytics will show reply and bounce rates after the fact, and the bounce rate is the honest verdict on your hygiene — how to read that number before it becomes a deliverability problem is in reducing your bounce rate. Until a send happens, a list is a hypothesis with a row count attached.

Frequently asked

How do you write an ICP that can actually be used to build a list?

Write it in fields rather than adjectives. "Fast-growing modern B2B companies" cannot be matched by anything; "job title contains VP Sales or Head of Sales, company headcount between 50 and 250, country IN or GB, industry Software" can be evaluated row by row. The test is whether a person with the spreadsheet in front of them could sort every row into in or out without asking you a question. If they could not, the profile is a description rather than a specification.

Why does a job-title filter return people with the wrong job?

Because most filters match substrings rather than whole words. A substring search for "hr" is satisfied by "Threading Systems Director", and the reverse failure is just as common — a filter for "CHRO" will not match "Chief Human Resources Officer" unless the alias is expanded. Word-boundary matching fixes the first problem and an alias table fixes the second. Check both by sampling thirty matched rows and reading the titles by hand.

What does a "risky" email verification result actually mean?

It means the verifier could not disprove the address and also could not prove it. Catch-all domains, role addresses such as info@ or support@, and addresses that pass every local check but cannot be confirmed all land here. It is not a synonym for bad. Treat risky as a segment with its own sending policy — lower volume, a warmed mailbox, and a close watch on bounces — rather than either deleting it or mixing it into your main send.

Can any verifier prove that a mailbox exists without sending to it?

Not reliably, and the standard itself says so. RFC 5321 section 3.5.3 states that a server "MUST NOT return a 250 code in response to a VRFY or EXPN command unless it has actually verified the address", and provides reply code 252 for addresses that appear valid but cannot be verified in real time. Many servers disable those commands entirely, and catch-all configurations accept every recipient at the protocol level. Certainty comes from a delivery being accepted, not from a probe.

How often should a lead list be re-verified?

Re-verify on a rolling window rather than all at once, because verdicts age at the rate people change jobs rather than at the rate of your calendar. A 30-day freshness window is a defensible default — Autocloz treats a stored verdict as fresh for 30 days and re-verifies past that. The GDPR's accuracy principle at Article 5(1)(d) requires personal data to be "accurate and, where necessary, kept up to date", which makes the hygiene cycle a compliance obligation in Europe rather than only a deliverability tactic.

Is a bigger lead list always better?

No, and the cost of a bad row is higher than the value of a good one at the margin. Every irrelevant address you send to costs sending reputation, consumes daily volume, and raises the complaint rate that receiving systems use to judge your domain. A 400-row list where every row matches the profile outperforms a 40,000-row list at 5% match, because the second one damages the channel that the first one depends on.

Share
Free to start

Stop reading. Start sending.

Every tactic in this article is implemented behind the Autocloz dashboard.