Read who a built audience reaches, as aggregates over a deterministic sample — and, on request, a shareable signal membership report. The read step behind /watt:audience; routes to the way in that fits what the user has — a brief, signals they already hold, or a list of people. Aggregates only — never individual records, never contact data, never an ad-platform export. Not a user command — /watt:audience is the front door. Use when a read-shaped ask arrives — "who's actually in this audience", "what do these people look like", "an audience profile for my client" — or to sanity-check a signal stack before exporting.
67
84%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
audience-analyze — the read step behind /watt:audience — answers the question generate can't: generate guaranteed an audience's size (or, for a profile, measured its headcount); this read shows what it means. The user walks away with a dashboard in two halves: your signals — the stack's own signals by share and how many each person hits — and discovered — the net-new traits that define these people against the world by lift, plus segmentation, skews, freshness. For a market profile, this read is the deliverable: on request it writes the two halves to a self-contained shareable report file.
There are three ways into that read, by what the user brings — and this skill routes to the one that fits:
audience-analyze-search.audience-analyze-signal.audience-analyze-list (discovered half only — no signals were specified).Route; don't run. Your job at this level is the routing question and the shared canon below — the language, the aggregates-only lane, and the read-and-report procedure every leaf composes with. The leaf does the discovery, the inline profiling run, and the render.
Called by: the /watt:audience router, or a sibling leaf's offer (audience-generate at its landing — a build to sanity-check, or a profile whose report is the deliverable; audience-activate after delivery) — with a built audience in session, a re-supplied audience record, or a fresh read-shaped ask.
Hands off to: the three leaves —
audience-analyze-search — brief → discover signals → organize into pools → operator pivot → materialize → read.audience-analyze-signal — a supplied stack/signal list → materialize → read (skips discovery).audience-analyze-list — a supplied list of people, as identifiers (resolve to entities) or as already-resolved entity IDs (a roster from grouping — skip the resolve) → discovered-only read.(After the read, the user continues into audience-generate to re-pivot or audience-activate to export — offered at the closing question, step 3.)
Shared across every leaf — the surface the user reads is always business language; the boolean operators never reach them.
| The user says | What it means |
|---|---|
| signal | a trait |
| must-have | an AND gate every person satisfies |
| exclusion | an AND_NOT — people to leave out |
| audience, market, the people | the person set the signal stack reaches |
| reach, market size, headcount | the count the build/resolve measured |
New to this surface: lift is explained once, in plain English — "how much more common a trait is in your audience than in the population — 5.6× means these people are 5.6 times likelier than average to have it" — then used freely. Sample is named honestly: for the search and signal flavors the read is computed over a fixed, reproducible slice of the audience, and the dashboard says how big; the list flavor reads the supplied set whole — no sampling — and the dashboard says that instead.
audience-analyze-signal, with that stack. Confirm which one in a word if there's any doubt, then go.audience-analyze-signal; take the role groups as the expression exactly as built, and a past session's figures are "measured then", not re-measured silently.audience-analyze-search, with everything they've said.audience-analyze-list — the one input the read can profile without composed signals; discovered half only. The list comes either as identifiers (a CSV / pasted emails — -list resolves them) or as already-resolved entity IDs — a roster from the grouping objective, or any entity-ID set the user holds — where -list skips the resolve and reads them directly. A roster carries group_label, so the whole set (its roster_uri) or a single group (its entity_ids_uri) can be read; either way there's no specified-signals half — these are people, not a signal stack the user named.audience-generate (build) or /watt:explore (just curious) honestly.Route silently when the shape is already answered — skip the question, never the handoff. Carry everything the user already said into the leaf; it must not re-ask it.
Each leaf reaches the same place — a built audience (a signal stack, or a resolved entity set) — and from there the read is identical. This is the procedure the leaves inherit; they don't restate it.
Aggregates only. Every number on screen — and in the report file — is computed over the audience as a whole; no individual record, identifier, or profile is ever pulled, shown, or written. A user who wants the people themselves is asking for audience-activate, behind its own confirmation. The report file is aggregates, not contact data — a different artifact from activate's PII export.
On the user's go (arriving at a leaf with a built audience is the go), run the profiling procedure inline — read it once from the bundle (cat "<Bundle root>/context/profiling.md", the path published at session start) and follow it here on the main thread:
expression_string, the stack's signals (hashes, names, roles), the location, the measured reach, and a shared workflow_id. Both halves come back.entity_ids_uri and the resolved count. The discovered half only — no signals were specified. For a roster, the entity_ids_uri is the whole roster_uri (the audience across its groups) or a single group's set; a per-group read is mode B re-run once per group's entity_ids_uri, the group_label naming each.Narrate it plainly ("Reading who your 2.4M actually are — drawing a fixed sample and comparing its traits against the world…"). Track the read as a session task; complete it when the aggregates are in hand.
The read carries section headings throughout — each part below sits under its own labeled heading (Summary, Your signals, Discovered, Skews, Freshness & band, Basis), in both the rendered dashboard and the shareable report file, so the read scans as a structured document and a reader can jump to the part they want.
Open with Summary — the 3–5 sentence plain-English read seeded from the procedure's findings — facts about who these people are, no recommendation. Then the dashboard — the surface the user reads is a rendered inline visual; the full read is captured in the shareable report file (written on request, step 3). The dashboard and the report both hold the procedure's halves, each under its heading:
Author the dashboard as a data-only visual: the sections under their labeled headings — Your signals and Discovered (the two halves), Skews, Freshness & band, and the Basis line in the frame, with Summary leading — a prevalence-vs-world bar and the lift figure beside each defining trait, your-signal shares and the coverage spread, the freshness mix. Show how the read was produced, one line: sample → your-signals membership + trait aggregation (intent split out, geo excluded when fenced) → lift against the world.
One question — landed as native clickable options — does this audience read right? The real options, when they fit: re-pivot (back into the audience-generate step, or back into the leaf's own pivot — the read is the evidence: drop a signal dragging in the wrong people, add an angle that's missing), save the shareable report (write the read to a self-contained file — the deliverable for a profile, and offered for any read), export it (the audience-activate step), read deeper (a domain the dashboard didn't cover — a fresh inline run), or done.
The signal membership report (the default). On the user's yes, assemble the procedure's read into a profile JSON — specified, coverage, discovered, breakdown (the procedure returns these field names already) — author the 3–5 sentence executive summary, and run the shared report script (build_report_membership.py — the default two-section signal membership report; future report types are build_report_<name>.py siblings). It lives in this audience-analyze skill's directory (not the leaf's), so any leaf runs it at ${CLAUDE_PLUGIN_ROOT}/skills/audience-analyze/scripts/build_report_membership.py; locate it if the runtime relocated the files. For a discovered-only read (the list flavor), pass --no-specified so the report drops the your-signals section:
python3 "${CLAUDE_PLUGIN_ROOT}/skills/audience-analyze/scripts/build_report_membership.py" --profile /tmp/profile.json \
--title "<client> — in-market buyers, <geo short>" \
--location-label "<human geo>" --expression "<human-readable expression>" \
--headcount <full reach or resolved count> --sample <sample size> \
--workflow-id <W> --gates '{"must_have":[…],"exclusion":[…]}' \
--summary "<exec summary>" \
[--no-specified] \
--out-html ./<client>_membership_report_<YYYYMMDD>.html \
--out-json ./<client>_membership_report_<YYYYMMDD>.jsonIt writes a self-contained HTML report (no JavaScript, no CDN — opens offline, aggregates only, no PII) plus a JSON audit. Write the deliverable into the working directory — a durable, discoverable home — not a temp path the OS can sweep; only the throwaway --profile input lives in /tmp. Surface the absolute path. For the standard report the math, filters, and layout are the script's — don't hand-rebuild what it already renders.
A custom report, on request. When the user wants something the standard report doesn't give — a different cut, extra or fewer stats, their own framing or layout — author a bespoke one on the fly: a self-contained HTML file (no JavaScript, no CDN, opens offline) crafted for the ask, written to the working directory. Two rules keep it honest: every figure traces to the profiling procedure's aggregates — the specified/coverage/discovered/breakdown you already hold, or a fresh inline run of the profiling procedure (context/profiling.md) for a cut it didn't cover, never a number you invented — and it stays aggregates-only — no row, no PII, like the standard one. The script is the consistent default; this is the escape hatch when the user asks for more.
-search, a refreshed stack in -signal — read the record contract first from the bundle: cat "<Bundle root>/context/record.md" (the path published at session start). It isn't injected at session start; read it once, then honor it for every record this session.audience-activate, named honestly; this step reads, it doesn't export.python3 missing or build_report_membership.py fails. Say so and offer the in-chat dashboard as the fallback — never hand-assemble the HTML to stand in for the script.audience-generate honestly rather than reading an empty set./watt:configure (it owns the connect path and recovery docs), and never loop the connect / authenticate tools, diagnose the MCP registry, or press on. (A pre-call notice isn't a failure; your first call settles it — never announce one.)880e260
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.