act · campaigns
Segments
Who a campaign runs on.
In Campaigns
A segment is the rule that decides who a campaign runs on. It is written once, evaluated against the lead table, and frozen into a member list at the moment people are assigned to arms. This page gives the rule grammar, the order exclusions run in, fourteen ready segments and what to record so the segment itself can be tuned later.
campaign library → segment · who → offer · what they get → arms · who gets what → stop and readout · when it ends
Exclusions run first — Never-contact, customers, competitors and the ICP's not-if lines are removed before any rule matches.
Freeze at assignment — Membership is a snapshot. A lead who qualifies tomorrow joins the next campaign, not this one.
Size before you launch — A segment too small to split into arms can only be read as a direction, never a result.
in industry terms
A segment is a saved filter over leads, frozen into a list when the campaign starts.
Marketing tools call it an audience or a list; sales tools call it a target account list or a smart view. The useful split is static vs dynamic: a dynamic segment is the rule, re-run every day; a static segment is the member list it produced at one moment. A campaign always runs on the static list, so its arms stay comparable.
definition — Segment rule · A boolean expression over lead fields: tier, trigger, signal age, source, fit lines, contact history.
snapshot — Member list · The leads that matched at freeze time, each with the exclusions it passed. This is what gets split into arms.
log — Exclusion log · Every eligible-looking lead that was left out, and the rule that removed it. These are negatives too.
how it works
From the whole lead table to a frozen list, in five passes.
Each pass is a filter. The order matters: hard exclusions first so no rule can accidentally bring a never-contact person back in.
- All leads — lead table
- Hard excludes — never, customers
- Contact policy — in a campaign, caps
- Rule match — tier, trigger, age
- Freeze — member snapshot
- Arms — random split removed
- Exclusion log — lead + rule removed
- Exclusion log — lead + rule
Then the ICP's not-if lines.
2 · contact policy — Already busy · Anyone in another, past their message cap, or who asked to stop.
3 · rule match — The segment rule · Tier, trigger, signal age, source, fit lines, engagement history.
4 · freeze — Snapshot · Write the member list with the rule version and time. Later changes to a lead don't move them between campaigns.
the rule grammar
One small language for every segment.
Every rule is clauses joined by and, or and parentheses. Fields come from the lead, its signals and its contact history. Keeping it this small means a rule can be stored, versioned, replayed on old data and shown to the founder in one line.
rule := clause ( ("and" | "or") clause )* | "(" rule ")"
clause := field op value | "not" clause
op := "=" | "!=" | "in" | "not in" | "<" | "<=" | ">" | ">=" | "exists" | "within"
value := number | text | "(" value ("," value)* ")" | "[" number "," number "]"
fields tier · trigger · signal_age_h · source · icp_fit · icp_line · contacted · last_touch_days
in_campaign · never_list · engagements_30d · follows_you · waitlist · days_since_join
trial_day · key_action_done · plan · ever_paid · stripe_event · rating · timezone · reply_class| Example | Reads as |
|---|---|
tier in (Hot, Warm) and contacted = false | every Hot or Warm lead nobody has messaged |
trigger = public_pain and signal_age_h <= 48 | posted the pain in the last two days |
icp_line = "solo founder" and not icp_line = "agency" | matches one fit line and none of a not-if line |
(source = reddit or source = x) and engagements_30d >= 2 | engaged twice on Reddit or X this month |
the object
The segment, and the snapshot it produces.
A segment is versioned like a playbook. The member list points back at the exact version that produced it.
| Field | Type | What it holds |
|---|---|---|
segment_id | string | seg.<name>, stable across versions. |
version | integer | Bumped on any change to the rule or exclusions. |
rule | expression | The rule in the grammar above, stored as text and as a parsed tree. |
excludes | list | Hard exclusions applied first: never list, customers, competitors, ICP not-if lines. |
freshness | hours | Maximum signal age, if the rule has one. Most cold segments do. |
frozen_at | timestamp | When the snapshot was taken, at campaign assignment. |
members | lead ids | The frozen list. Each member carries the rule version and its signals at that moment. |
excluded | list of {lead, rule} | Who looked eligible and was removed, with the rule that removed them. |
size | integer | Member count at freeze. Drives whether arms are possible (see Arms and holdouts). |
one record, as data
{
"segment_id": "seg.warm-never-contacted",
"version": 2,
"rule": "tier in (Hot, Warm) and contacted = false and source in (reddit, x)",
"excludes": [
"never_list",
"customers",
"competitors",
"icp.not_if"
],
"freshness_h": null,
"frozen_at": "2026-10-20T13:00:00Z",
"icp_version": "icp.yourproduct.v3",
"size": null,
"members": [
"lead_…"
],
"excluded": [
{
"lead": "lead_…",
"rule": "in_campaign"
}
]
}the catalog
Fourteen segments a solo founder can start from.
Cold segments find strangers showing a trigger; warm ones are people already in your funnel; customer segments run on Stripe events; partner segments are founders, not buyers.
seg.hot-fresh-pain — Cold
Hot leads with fresh public pain
People who posted the exact pain in the last 48 hours and pass every fit line.
- rule
tier in (Hot) and trigger = public_pain and signal_age_h <= 48 and contacted = false- used by
- Reddit pain-reply sprint, Lead to waitlist
- size
- small; refresh daily
seg.warm-never-contacted — Cold
Warm leads nobody has contacted
Fit and a real signal, but weaker or older. The bulk of most queues.
- rule
tier in (Hot, Warm) and contacted = false and not in never_list- used by
- Lead to waitlist
- size
- the largest cold segment
seg.competitor-unhappy-7d — Cold
Unhappy with a competitor, this week
Complained publicly about a named competitor in the last 7 days.
- rule
trigger = competitor_complaint and signal_age_h <= 168 and icp_fit = true- used by
- Competitor-unhappy switch offer
- size
- depends on how loud the category is
seg.review-low-30d — Cold
1 to 3 star reviewers, last 30 days
Reviewers of a competitor on G2, Capterra or the app stores who can be found publicly.
- rule
source in (g2, capterra, app_store) and rating <= 3 and signal_age_h <= 720 and identity_found = true- used by
- Review-site responders
- size
- small; found
seg.alt-searchers — Cold
"Alternative to" askers
People asking for an alternative to, or a comparison with, a named competitor.
- rule
trigger in (asks_alternative, comparison_question) and signal_age_h <= 72- used by
- Comparison-page interception
- size
- steady trickle
seg.launch-week — Cold
Founders in launch week
Launched on Show HN, Product Hunt or a directory in the last 7 days, in your ICP.
- rule
trigger = launched and signal_age_h <= 168 and icp_fit = true- used by
- Launch-week offers, Founder swap
- size
- weekly batches
seg.engaged-twice — Cold
Lurkers who engaged twice
Liked, replied or bookmarked your posts at least twice in 30 days but never talked to you.
- rule
engagements_30d >= 2 and contacted = false and follows_you in (true, false)- used by
- Waitlist countdown follow-ups
- size
- grows with your posting
seg.keyword-commenters — Warm
Keyword commenters
Everyone who commented the keyword on an offer post. They asked; the clock is short.
- rule
trigger = hand_raise and post_id = {offer_post} and signal_age_h <= 24- used by
- Comment-a-keyword lead magnet
- size
- whatever the post earns
seg.waitlist-unactivated — Warm
Waitlist joiners who haven't come back
Joined the waitlist and never opened the follow-up or answered its question.
- rule
waitlist = true and days_since_join >= 3 and replied = false- used by
- Waitlist countdown, beta recruit
- size
- grows with the waitlist
seg.stalled-trial-d3 — Warm
Stalled trials, day 3
Started a trial and haven't done the key action by day 3.
- rule
trial_day >= 3 and key_action_done = false and plan = trial- used by
- Stalled-trial rescue
- size
- after launch only
seg.failed-payment — Customers
Failed payments
Every customer with a failed invoice. Never held out: the money is owed.
- rule
stripe_event = invoice.payment_failed and resolved = false- used by
- Failed payment recovery
- size
- always on
seg.cancelled-30-90 — Customers
Cancelled 30 to 90 days ago
Past customers far enough out to have cooled down, close enough to remember you.
- rule
stripe_event = customer.subscription.deleted and days_since in [30, 90] and never_list = false- used by
- Win-back
- size
- small
seg.former-paying — Customers
People who paid for an earlier version. The warmest list a relaunching product has.
- rule
ever_paid = true and plan = none- used by
- Relaunch announcement, waitlist early access
seg.partner-founders — Partners
Partner founders
Founders who sell to the same buyers with a product that doesn't compete, close to your size.
- rule
kind = founder and audience_overlap = high and competes = false and size_ratio in [0.5, 2]- used by
- Founder swap
- size
- a short list, built by hand
failure modes
What goes wrong, and how to catch it.
How each one shows up, and the check that catches it before it costs a week.
| Failure | How it shows up | How to catch it |
|---|---|---|
| Too broad | Lots of offers, few replies; reply rate falls as the segment grows | Track reply rate by tier and signal age inside the segment; tighten the rule where it drops |
| Leakage between campaigns | The same person gets two sequences in a week | Freeze membership and exclude anyone in_campaign before matching |
| Stale membership | Leads contacted days after their signal, replies dry up | Put freshness in the rule, and refreeze for each new campaign |
| Holdout overlap | A held-out lead gets a message from another campaign | Check the exclusion log: held-out leads must stay excluded for the window |
| Too small to split | A segment of 20 split 90/10 leaves a holdout of 2 | Read it as a direction, or pool several weeks before judging |
| ICP drift | Fit lines change, old segments keep the old meaning | Version the segment with the ICP version it was written against |
data to capture
Record it so a model can learn from it later.
Follows the guide's eight data-spine rules: log the decision, version what decided, freeze the inputs, keep the negatives, log the odds, treat every human touch as a label, keep one chain of ids, keep the raw.
record
- the rule text, its version, and the ICP version it was written against
- the frozen member list, with each member's signals and fit lines as they were at freeze
- every excluded lead and the rule that removed it
- segment size at freeze, and how many members actually got a touch
label
conversion per member; members who were eligible but excluded
trains
which clauses predict replies and payment (rule tuning), and eligibility models that score who belongs in a segment
test
alternative rules over the same lead history · offline replay of a new rule on stored leads, then a