| name | audience-generate-search |
| description | Build a person audience from a plain-English description — discover the signals behind the brief, score them, and compose, partition, or cross them into a measured result. The discovery-driven way into a build, behind /watt:audience; anchored on a description (no owned list). The objective picks how it lands — a size band, the widest credible reach, the highest-precision few, grouping into the best-concentrated cells, or crossing the employment graph to their employers (or employees). Produces a signal stack + measured reach, or a roster — never an export, never contact data. Not a user command. Use when a build starts from a description — "build me an audience of about 1–5M weekend hikers", "the highest-intent people in-market for solar", "where do these people cluster", "a lead list of in-market companies". |
| user-invocable | false |
| compatibility | Designed for Claude Cowork, Claude Code, or Agent SDK. Requires Signal Graph. |
Build an audience from a description
Purpose
audience-generate-search is the build leaf anchored on a plain-English description — the user arrives with who they want to reach, not an owned list. It runs the parent's shared discovery and scoring, then composes — or partitions — the approved working set into a measured result.
The leaf is split from its sibling by input anchor (description here; an owned list is audience-generate-list), and inside it the objective picks the strategy. Two output families ship today:
- A signal stack — the boolean shape of one audience and its measured reach. The landing mode picks the compose strategy: a size band composed with greedy (signals added one at a time, reach measured after every step, until the audience lands inside the band), maximum credible reach composed with broad (the core ORed into a wide pool, dead-weight skipped, lightly gated, the union flagged when it swells toward the whole universe), or precision composed with lift (from the scored pool, the signals with the most lift over a must-have base are picked, for the highest-propensity audience).
- A roster — an entity set with classification, from a Classify strategy. The grouping objective ("where do they cluster", event/site-selection) partitions the brief's people into the best-concentrated groups with group (disjoint cells across one or more axes; entity IDs + group label + score inputs). The crossing objective (a lead list of in-market companies, employees of these firms) composes a seed audience and crosses the employment graph to the related entities — employers of these people, or employees of these companies — then keeps those that pass a target-side filter with traverse (the qualified set: entity IDs + source provenance, membership only — ranking or segmenting that set is a separate strategy). Both return a roster with an ID-only
roster_uri.
This is a delta over audience-generate: the unique work here is the objective, the strategy that hits it, and — for the roster objectives — the partition (grouping) or the graph crossing (crossing) that the stack objectives don't have. Discovery, scoring, the working-set record, the pivot loop, and the landing are the parent's shared spine (audience-generate → The shared spine), composed with verbatim — not restated. The inputs are the audience description and the objective's target (a band for greedy; no target for broad; a must-have base + output mode for lift; the grouping dimension for group; the crossing direction + optional target filter for traverse); an /watt:explore signal pool — or a lookalike signal pool from audience-generate-list — carries in as a pre-seeded working set, discovery skipped for the angles it covers.
Works with
- Called by: the
audience-generate router, when the build starts from a description (the default anchor today) — carrying an /watt:explore signal pool, or a lookalike signal pool from audience-generate-list, as the pre-seeded working set when one came through.
- Runs inline (all on the main thread, no subagent — the objective the user picks selects which strategy procedure; the compose offer is a strategy-picker, not a hard-named one). Each procedure is read on demand —
cat "<Bundle root>/context/<file>", once this session — then run:
- greedy (band mode → signal stack,
context/compose-greedy.md) — the working set (in scored order) + the band → the signal stack, its measured reach, the assembly trace, and an ID-only sample.
- broad (max-reach mode → signal stack,
context/compose-broad.md) — the working set (in scored order), no band → the wide-OR stack, its measured reach, the assembly trace, a credibility read (gated? what fraction of the universe? promotable anchor candidates), and an ID-only sample. Returns needs_anchor when an ungated union has swollen toward the whole universe — that's a finding to put to the user, not an off-ramp.
- lift (precision mode → signal stack,
context/compose-lift.md) — the working set (must-haves as the base, candidates scored) + the output mode → the lift-ranked candidates, the picked signals, the composed expression, and its measured reach. Lift over the base is computed from reach counts, not calculate_trait_lift.
- group (grouping objective → roster,
context/partition-group.md) — the base set's expression + the resolved axes (the named dimension turned into its trait-expression buckets — the discovery step's job, not the partition's) → the roster: the top-K disjoint cells with group label, cell_lift, cell_size, and rank, an ID-only roster_uri, and the pruned/coverage counts. Partition-type axes only; overlap-type (interest/intent) is declined. The grouping dimension is always the user's (named here, or surfaced by an audience-analyze read first); the partition never discovers it, and never resolves the traits behind it.
- traverse (crossing objective → roster,
context/cross-traverse.md) — a composed seed set (its expression or entity_ids_uri) + the relationship and source/target pair + an optional resolved target filter → the roster: the whole qualified set of crossed entities that pass the filter, each with its source provenance, an ID-only roster_uri, and coverage (seeds, joined fraction, qualified). Membership only — traverse never scores, ranks, or truncates (no score_spec, no top-K); it's a compose-seed → cross chain (compose the seed first, then cross). Ranking the set into a prioritized lead list or segmenting it is a separate Classify step over the roster (group segments; ranking is the roster re-entering audience-generate-list's overlay play against a target-side signal pool). The supported crossings are whatever entity_relations exposes (today employs, person↔business); read them from the tool, never an allow-list.
- (The shared discovery and scoring are the parent's — this leaf runs them through the shared spine, all inline on the main thread too: discovery via
context/discovery.md, scoring via context/scoring.md, adjacencies via context/adjacencies.md.)
Language
Inherits the parent's table (must-haves / defining signals / exclusions; place as a geo-boundary signal; signal ↔ trait). The terms this leaf owns:
- Stack objectives — the landing mode: a size band (and the band itself, a floor and a ceiling, the contract greedy converges to); maximum credible reach (no band; surface terms the widest credible audience and, when broad flags an over-broad union, a must-have to anchor it); or precision (the base — the must-haves lift is measured over — and whether to tighten, the high-propensity slice of the base, or reach more like them).
- Grouping objective — surfaces as where do they cluster / the top groups; the user names the grouping dimension in plain words (by region, by age, by income — or several at once). When they're unsure which dimension, that's a read first (
audience-analyze shows what defines these people), not a guess. The result is the best-concentrated groups, each with how concentrated (lift) and how many (size). Say groups and the dimension to group by — never "cells", "partition", "cross-product", or "axis" at the surface.
- Crossing objective — surfaces as a lead list, in-market companies, the employers of these people, or employees of / staff at these companies. The user describes a seed audience (people, or companies) and which way to cross — to their employers or to their employees — plus, optionally, how to narrow the crossed side (at mid-size firms, in tech). The result is the set of companies / people that qualify, each tagged with who it came from. Say cross to their employers / employees and the set that qualifies — never "traverse", "edge", "the roster", or "entity_relations" at the surface. (To prioritize that set — "the top 500" — or break it into segments is a further step, named when the user asks for it.)
No AND/OR/NOT, no boolean "pools" (the OR/AND/AND_NOT groupings — signal pool, the kept-signals carrier, is the user's word and fine), no "output mode" at the surface.
The flow
1 — Settle the objective and its target
Five objectives ship — three compose a signal stack, two produce a roster (one partitions, one crosses Signal Graph). The brief usually settles which; route silently when it does, and make it the turn's pick only when it's genuinely open.
- Size band (→ greedy, signal stack) — "about 2M weekend hikers" or any size in the ask. Settle the band: "Target size, low and high — e.g. 1,000,000 to 5,000,000?" Validate it — whole numbers,
1,000 ≤ low < high ≤ 200,000,000; a single number → ask for a range (the band is what makes the assembly honest about where it lands).
- Maximum credible reach (→ broad, signal stack) — "the widest credible audience", awareness, reach, top-of-funnel. No target to settle; the reach is whatever the credible signals support, and broad measures it. Go straight to discovery.
- Precision (→ lift, signal stack) — in-market, high-intent, consideration, conversion, "ready to buy". No size target; the target is settled at the compose offer (step 3): which must-haves form the base, and whether to tighten or reach more like them. Precision needs at least one must-have as the base — if discovery produces none, that's the leverage surfaced at compose. Go to discovery.
- Grouping (→ group, roster) — "where do they cluster", "which regions over-index", "break them down by …", event/site-selection. The audience here is the base set (everyone the brief describes); group ranks the best-concentrated sub-groups inside it across the grouping dimension(s) the user names — by region, by age, by income, or several at once (the cross of them). If the user isn't sure which dimension to group by, don't guess and don't have group sweep for it — that's a read first: route to
audience-analyze (its -search flavor) to see what defines these people by lift; the dimensions that over-index point to how to group, and they come back here to group by the one the read surfaced. The grouping dimension is settled (named, or revealed by that read) before the compose.
- Crossing (→ traverse, roster) — "a lead list of in-market companies", leadgen, accounts / ABM, recruiting, "the employees of these firms". The brief settles the direction: an ask about companies / employers / a lead list crosses people → their employers; an ask about employees / staff / who works at crosses companies → their people. The supported crossings are whatever
entity_relations exposes (today employs, both ways); a direction the tool can't honor is named honestly, not faked. The seed audience (the side you start from) is composed first with whichever compose strategy fits the brief — the seed and the optional target-side filter are both settled before the cross. Two settle-points carry to step 3: which way to cross, and (optionally) how to narrow the crossed side.
When the objective itself is open, the pick is a target size, the widest credible audience, the highest-propensity people, where these people cluster, or the set reached by crossing the employment graph (their employers / their employees)? — band the default when a size was floated.
One read to route away, not build: if the user wants to understand a market — who one audience is, its overall traits — that's audience-analyze (its -search flavor), not grouping (grouping ranks sub-groups of a target audience; profiling describes one). A pure indifference to size, reach, and propensity with no grouping ask is also a read. Name audience-analyze and route there rather than inventing a band.
The audience description plus the objective's target (the band; nothing for max-reach; the base/output-mode for precision; the grouping dimension for grouping; the crossing direction + optional target filter for crossing) are the inputs — with them in hand, go to discovery.
2 — Run the shared discover & score
Hand to the parent's shared spine (audience-generate → The shared spine, steps A–C): read the brief into angles, run discovery inline per angle (or once in full scope on the user's sweep-it-all ask), score the pool per role with the inline scoring procedure, render the scored slice, and run the working-set record + pivot loop. The objective rides in the working-set header — target: band <low>–<high> (band), target: max-reach (max-reach), target: precision (base …) (precision), target: grouping (<dimension>) (grouping), or target: crossing (people→employers, filter …) (crossing) — so it survives compaction.
The crossing objective discovers on two entity types: the seed side (the audience you start from — discovery with entity_type matching the seed) and, when the user wants to narrow the crossed side, the target-filter side (a second, smaller discovery pass on the other entity type — business firmographics when crossing to employers, person signals when crossing to employees). Each is its own working set with its own header (seed: / target-filter:); the seed is the primary, the filter optional. Everything else in discovery — angles, the scoring, the pivot loop — is the parent's, run once per side; don't restate it.
A pre-seeded pool arrives without compose scores — and a lookalike pool also arrives without roles. An /watt:explore pool carries its roles in session; honor them and skip the role beat (re-asking what the handoff already answers violates it). A lookalike signal pool does not — its halves (durable identity / intent layer) are not a role split — so settle its roles with the user in one beat: which signals are must-haves or exclusions; the rest land core (durable-identity signals usually map to core, a life-stage or geo gate to must-have). Either way, score the pool per role through the spine's inline scoring exactly as a discovered pool would be — lift-or-reach figures are profiling metrics; never carry them forward as a score.
The one thing this leaf supplies to the shared scoring is the core pool's breadth lean — passed as the breadth element of the core role's weights vector in the spine's scoring call. The parent leaves it to the objective leaf, and the objective sets it:
- Band mode — read the band's midpoint against the addressable US-adult universe: a tight target leans the core toward precise, narrow signals (negative
breadth weight, so distinctiveness wins); a broad target leans it toward broad signals (positive breadth); a middling target sits near zero.
- Max-reach mode — lean strongly breadth-positive: broad wants big, fresh, high-reach signals at the top of the OR pool, so the wide union forms from the signals that actually move reach, with the niche tail skipped at the marginal floor.
- Precision mode — lean toward distinctiveness and recency (negative
breadth, a firmer freshness positive): lift rewards candidates that are over-indexed in the base and fresh, so narrow, current signals are the ones worth scoring high.
- Grouping — lean breadth-positive: the audience here is a base set to partition, not a precision target, so it wants good coverage of everyone the brief describes (a thin base set makes for thin, low-confidence groups). Not as strong as max-reach — the base set should still be on-brief, not the whole universe.
- Crossing — the seed side's lean follows the compose strategy chosen to build it (a
lift in-market seed leans toward distinctiveness + recency; a broad seed leans breadth-positive); the optional target-filter side leans toward distinctiveness (it narrows the crossed set, it doesn't size it). The crossing's value is a clean, on-brief qualified set, not seed size — a vast vague seed crosses to noise.
It's a directional lean, not a precise formula — the strategy still measures its way to the result. Everything else in discovery and scoring is the parent's; don't restate it.
3 — Compose or partition — the objective's strategy
When every angle is covered or consciously dropped, offer the step — naming the strategy the settled objective selects, not asking which to use (the objective already answered that). The offer is a strategy-picker: the objective names the strategy, and when the objective is genuinely open the strategies are the turn's pick. Adding a strategy is a thin delta to this offer — register it, wire its edge, add its vocab row — never a rewrite.
- Band mode — "Working set's ready — 7 signals across both angles. Compose it toward 1–5M with the greedy strategy?"
- Max-reach mode — "Working set's ready — 7 signals. Compose the widest credible audience these support with the broad strategy?"
- Precision mode — settle the base and output mode in the offer: "Working set's ready. I'll measure each signal's lift over your must-have base [name it] and keep the few that move it most — do you want the highest-propensity slice of that base, or to reach new people who match on those signals?" Precision needs a must-have base — if the working set has none, that's the turn's question first: name a must-have, or switch to a band or max-reach.
- Grouping — "Working set's ready — 7 signals. Build the base audience and rank the top groups inside it, by region × age?" (the offer always names the dimension the user settled in step 1).
- Crossing — "Seed's ready — 7 signals for in-market solar buyers. Compose them, then cross to their employers and pull the companies that qualify?" (the offer names the direction the user settled, and the target filter if any).
Only on yes, run the objective's path.
Stack objectives (band / max-reach / precision). Run the selected compose procedure inline with the working set (in scored order, pivots applied) — greedy (context/compose-greedy.md) with the band, broad (context/compose-broad.md) with no band, or lift (context/compose-lift.md) with the must-haves as the base, the chosen output mode (tighten = slice of the base; reach more like them = structural anchor retained), and the default select (the few highest-lift). Narrate the run as it reports ("Adding 'In-market: Hiking Gear' — the union now reaches 1.4M…"; or "'Interested: Sustainability' lifts the base 4.7× — keeping it…"). Render the trace as the story of the build: each step, the reach after it, where it landed — and the leftovers it never needed (for broad, the core signals skipped at the marginal floor, named honestly as adding no real reach; for lift, the lift-ranked table with every candidate's lift over the base and the few picked). Lift's multiple is over each candidate's own base rate, so present it as an honest approximation, not an exact figure. Reach is measured at every step; never present arithmetic over per-signal sizes as a count. Handle the procedure's edge report as the turn's question — never quietly accept it:
- Greedy reports
below_floor / above_ceiling — show its note and put the leverage to the user: expand an angle, relax the band, drop a broad exclusion, or accept the off-band result as-is. Never change the band to fit what composed — the band is the user's.
- Broad reports
needs_anchor — the union is ungated and reaches a majority of the addressable universe (it's approaching everyone). Show the reach and the fraction, and put the stack move to the user, with broad's anchor_candidates as the signals driving the breadth: promote one to a must-have (anchor the audience on it), add a structural must-have the pool doesn't yet hold (an occupation, life-stage, or place — a fresh discovery pivot), exclude a signal, or accept the wide-open reach as intended (broad's whole job is reach — a confirmed big number is a valid landing). A credible status lands straight away.
- Lift reports
no_base / base_below_floor / no_lift_candidates — show its note and put the leverage to the user: add or broaden the must-have base, deepen an angle for stronger candidates, or switch to a band or max-reach. When nothing lifts the base, the base itself is the honest audience — say so rather than forcing a pick.
Grouping objective. Two inline runs, narrated as one story:
- Compose the base set. Run the broad compose inline (
context/compose-broad.md) with the working set, no band — the credible audience of everyone the brief describes; its measured reach is the base set's size. Unlike the max-reach objective, a wide or ungated base is accepted as intended here (it's the population to partition, not a targeted audience) — a needs_anchor base lands as the base set, not a problem to surface. It is still recorded honestly: the roster record's header carries the base as base 1.4M (ungated) when it landed needs_anchor, so the downstream read knows the population carries no anchor.
- Resolve the dimension, then partition. First turn the user's named dimension into its buckets through the shared discovery — discovery resolves "age" into Signal Graph's age buckets (a band is the OR of its single-year
age traits), "region" into state hashes, "income" into its banded cuts; each bucket is a label + a resolved trait expression, mutually exclusive within the axis. Then run the group partition inline (context/partition-group.md) with the base set's expression as source_expression and those resolved axes — the partition ANDs the buckets, it never looks a trait up. Narrate the partition ("Grouping the 1.4M base set by region × age — 138 groups, 31 above the size floor…"). The partition returns the roster: the top-K best-concentrated groups, each with its label, how concentrated (cell_lift), how many (cell_size), and rank. Grouping by a single dimension is measured exactly across the full base at any size. A cross of two or more dimensions on a base above 50K uses a bounded 50K sample only to pick the top groups worth measuring; each winning group's size and concentration are then measured exactly on the full base, and its people pulled from the full set — so no number shown ever rode on the sample. Either way the roster is complete; the sample only chooses which groups win. Show it — the groups ranked, the lift and size driving each rank both shown — and surface the honest limit every run: how fine a cross the base size credibly supports (a bigger base earns a finer cross, not a bigger sample), and the two ways a group was suppressed, kept distinct — too thin in the sample to trust (below_support, noise) versus a real population under the size floor (below_floor) — plus the coverage (assigned vs. unassigned). Surface any note plainly — a geo boundary type not supported, an overlap-type dimension declined (interest/intent grouping is not supported), or a dimension that proved multi-valued.
Re-composing or re-grouping after any change is a fresh run of the same procedure.
Crossing objective. Two inline runs, narrated as one story — compose the seed, then cross it:
- Compose the seed. Run the compose procedure that fits the seed brief inline — lift (
context/compose-lift.md) for an in-market seed (the canonical chain), greedy (context/compose-greedy.md) / broad (context/compose-broad.md) when the seed is a sized or wide audience — with the seed working set. Its stack (the expression, and the materialized entity_ids_uri) is the seed you cross from; render it as its own short trace, then move to the cross.
- Cross and filter. Run the traverse cross inline (
context/cross-traverse.md) with the seed as source (its expression or entity_ids_uri), the relationship and the source_entity_type / target_entity_type pair the direction settled (today employs; people→employers, or companies→people), and the resolved target filter (the second working set, or empty). Narrate the cross as it reports ("Crossing 180K in-market people to their employers — 50K joined; keeping the mid-size tech firms → 9.1K qualify…"). The cross returns the roster: the whole qualified set (every crossed entity that passed the filter, unordered), each tagged with its source provenance. Show it — the qualified set, its size, the seed→target provenance, the coverage — and surface the measured counts every run — the observed join fraction, reported as measured, never compensated. Surface any note plainly — a direction the tool can't honor, an empty join, a thin post-filter result, or a seed that didn't materialize.
The qualified set is the deliverable here; prioritizing it into a ranked shortlist or breaking it into segments is a further step, not part of the cross. If the user wants "the top N companies," say the set is in hand and the ranking is a separate pass — re-enter the roster through audience-generate-list's overlay play (score against a target-side signal pool, ranked), sort by provenance count (a deterministic read of the roster's own source_provenance — most in-market employees first), or segment it with the grouping objective; don't fake an order traverse didn't produce. Re-composing the seed or re-crossing after any change is a fresh run of the relevant procedure.
4 — Land the result
A signal stack (band / max-reach / precision). Hand back to the parent's shared landing (audience-generate → The shared spine, step E): write the audience record file, render the landed audience, and offer the downstream (audience-analyze / audience-activate). The record header carries the landing mode: reach 2.4M (band 1M–5M) in band mode, reach 84M (max-reach) in max-reach mode, reach 180K · precision (lift over base) in precision mode; a needs_anchor union the user accepted as wide-open is recorded as max-reach, ungated so the downstream read knows it carries no anchor.
A roster (grouping or crossing). The output is a roster, not a signal stack, so what's saved is the roster record — the shared serialization every roster-emitting strategy (group and traverse today; overlay / expand as they ship) writes, so audience-analyze and audience-activate consume any of them the same way. Its classification columns vary by strategy — group's are group_label, cell_lift, cell_size, rank (ranked groups); traverse's are source_provenance only (an unordered qualified set — no rank or score, since traverse is membership-only) — but the frame (a header naming the build, the rows, honest coverage, an ID-only roster_uri) is shared. Show the rows and write the roster record per the record contract (context/record.md).
Grouping:
# Watt record · kind: roster · play: group · audience: weekend hikers · grouped by region × age · base 1.4M
# dimensions: geo.state × demographic.age_band
# top groups picked on a 50K sample (base 1.4M); sizes & lift measured exact on the full base pruned: 18 below support (60K, noise), 107 below 1,000 (220K) coverage: 402K of 1.4M assigned to top 10
# roster_uri: workflow://…/roster.csv columns: entity_id, group_label, cell_lift, cell_size, rank entity_type: person
rank,group_label,cell_lift,cell_size,entity_ids_uri
1,CO · 25–34,3.1×,44K,workflow://…/cell_01.csv
2,WA · 25–34,2.6×,38K,workflow://…/cell_02.csv
Crossing:
# Watt record · kind: roster · play: traverse · audience: in-market solar buyers → employers · filter: mid-size tech firms
# crossing: employs (person→business) seeds 180K · joined 50.4K · qualified 9.1K
# roster_uri: workflow://…/roster.csv entity_type: business
# next: rank into a shortlist or segment the set — a separate step, on request
entity_id,source_provenance
b_8f…,3 in-market employees
b_2a…,2 in-market employees
… (sample; full set behind roster_uri)
The roster record is the roster's canonical serialization: its classification columns are first-class and must survive downstream, the roster_uri carries the full entity-ID set (IDs only — never contact data) as a CSV of those columns, and the # crossing: / coverage line stays honest about the measured join and what was filtered out. A grouped record's rows each carry that group's entity_ids_uri (the entity_ids_uri column — the cell artifact from the partition's return, the cell_01.csv values above) — that URI is what a per-group read (audience-analyze-list) or a per-group export (audience-activate) consumes; the flat roster_uri carries the per-entity classification columns, never the per-group URIs, so dropping the entity_ids_uri column from the record severs the per-group paths. Then offer the downstream in one line: read who these entities are over the roster (audience-analyze); export the set (audience-activate); or shape it further — segment it with the grouping objective, or rank it into a shortlist (the roster re-enters audience-generate-list's overlay play). Either of the first two runs on their say-so.
How to behave
- The target is the user's. In band mode, never quietly land off-band or change the band to fit what composed. In max-reach mode, never quietly narrow an over-broad union — broad's
needs_anchor is a finding to surface with its anchor candidates, and "accept wide-open reach" is the user's to choose, not yours to assume. In precision mode, the base and the output mode (tighten / reach more like them) are the user's pick — never swap them silently. In grouping, the dimension is the user's — they name it (or an audience-analyze read surfaces it first); group never discovers or auto-picks it. In crossing, the direction and the target filter are the user's — never flip the crossing or narrow the crossed side on your own.
- The strategy converges by measurement. Greedy honors the band in the assembly loop; broad maximizes reach by the same measured loop, skipping signals that add none; lift ranks candidates by lift over the base from reach counts — never
calculate_trait_lift (intent signals have no world baseline there); group measures every cell's size and lift directly (the joint cell, never arithmetic over the marginals); traverse crosses real employs edges, reports the observed join — never an assumed rate — and filters to the qualified set (membership only — it never scores, ranks, or truncates; ordering the set is a separate strategy). Per-signal scores rank; the probe's total is the only truth about a combination — and broad's credibility fraction is measured against a geo-correct universe, never assumed.
- End every turn at its question — the objective, the band or base/output-mode or grouping dimension or crossing direction, the compose gate, off-band leverage, broad's stack move, lift's edge, the cross's join finding. A result the user didn't steer is the failure, not the deliverable.
- Don't restate the parent. Discovery, scoring, the working-set record, and the landing are the shared spine; this leaf adds only the objective, the strategy that hits it, the partition for grouping, and the graph crossing for crossing.
- Five objectives today. Band (greedy), max-reach (broad), and precision (lift) compose; grouping (group) partitions; crossing (traverse) crosses the employment graph to a qualified-set roster (membership only — ranking or segmenting the set is a separate, downstream strategy, not part of the cross). The supported crossings are whatever
entity_relations exposes (today employs, both directions) — don't offer a crossing the tool can't honor.
- Signal stack or roster, not contact data. This leaf ends at a stack + measured reach + an ID-only sample, or a roster of ID-only entities + their classification — never records or files. Materializing contact data is
audience-activate's lane.
- US-only, adults-only, person audiences only.
Refuse cleanly
- "Skip the band — just build it." Not a refusal — it's a band-free landing mode: max-reach (the widest credible audience, broad) or precision (the highest-propensity people, lift). Offer them by intent — reach vs. propensity; neither needs a band. (If they instead want to understand a market rather than target or reach one, that's
audience-analyze (-search) — route there.)
- "Where do they cluster / which regions / break them down by …" Not a refusal — it's the grouping objective: build the base audience and partition it into the best-concentrated groups with group. Offer it explicitly.
- "Group them by what they're into / their interests / what they're in-market for." Interest, intent, and affinity are overlap-type — a person carries many, so the groups would overlap. Not supported; say so and offer a partition-type dimension instead — region, age, income, life-stage, homeownership.
- "Just give me the list / the emails." Generate builds and measures; exporting people is the activate step, behind its own scale-and-identifiers confirmation.
- A profile / "how many are out there." That's a read — route to
audience-analyze; this leaf composes or partitions toward a target.
- An owned list of people / a CSV as the seed. That's the
audience-generate-list anchor — route there (it resolves the list to a matched roster). To read a supplied list as aggregates, audience-analyze-list; to build from a description, give the brief.
- A direct business audience — "companies that …" by firmographics alone. A business set isn't a direct build here — but a lead list of companies whose people match (in-market, a behavior, a role) is the crossing objective: anchor on the people and cross to their employers. Offer that. (Targeting people by a job-title trait as the defining criterion is still a redirect to interest / demographic / location — that's trait-level targeting, separate from the employment-graph crossing the crossing objective uses.)
- Non-US / minors. Decline as everywhere in the plugin.
Failure modes
- A reach probe errors mid-compose. The procedure halts and surfaces it; show where the build stopped and what was measured up to that point — never fill the gap with an estimate. The one exception is broad's universe probe: if it can't size the universe, broad reports
needs_anchor conservatively rather than waving an ungated wide union through — surface that as the over-broad finding, not as an error.
- A partition probe errors mid-grouping. group halts and surfaces the failing call — show the base set size and which step stopped; never substitute a guessed cell count or a hand-computed lift. A requested dimension group can't honor — an overlap-type interest/intent axis, or a geo boundary type Signal Graph doesn't hold — comes back as a
note, not a fabricated grouping; relay it and offer a supported dimension.
- A crossing or filter probe errors mid-cross. traverse halts and surfaces the failing call — show the seed size and which step stopped (materialize / cross / filter); never substitute a guessed join rate or count. A direction the tool can't honor, an empty join, or a thin post-filter result comes back as a
note, not a fabricated roster; relay it and put the next move to the user — broaden the seed, drop a target-filter must-have, or accept the smaller set.