From Watt
Export a built audience as a platform-ready file — confirms the platform, the scale, and the identifier types with the user, then materializes the audience and runs the deterministic writer script, returning the finished file and honest row counts. Meta, Google, and Reddit are the supported platforms. Never runs unconfirmed. Not a user command — /watt:audience is the front door. Use when an export-shaped ask arrives — "export it", "push it to Meta", "push it to Google", "push it to Reddit", the audience as a file.
How this skill is triggered — by the user, by Claude, or both
Slash command
/watt:audience-activateThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
`audience-activate` — the export step behind `/watt:audience` — turns a built audience into the deliverable: a file shaped to the target platform's spec by its bundled writer script — one script per platform under `scripts/writers/`. The writer scripts present are the platforms that ship — **Meta, Google, and Reddit**, each its own self-contained writer script. The user picks the platform and c...
audience-activate — the export step behind /watt:audience — turns a built audience into the deliverable: a file shaped to the target platform's spec by its bundled writer script — one script per platform under scripts/writers/. The writer scripts present are the platforms that ship — Meta, Google, and Reddit, each its own self-contained writer script. The user picks the platform and confirms exactly what will happen, and walks away with the file, its row counts, and the reproducibility handle.
Nothing exports unconfirmed. Before any dispatch, state plainly: the platform, roughly how many people, which identifier types ride along, what the platform's spec does to them, and the exact row ceiling. Each platform's writer owns that transform, and they differ — confirm the one the user picked: a Meta file hashes emails, phones, names, cities, states, zips, and country, with mobile-ad IDs riding raw (the one identifier Meta keeps in the clear); a Google file hashes emails, phones, and names but keeps country and zip in the clear, with mobile device IDs written to a separate unhashed list; a Reddit file hashes emails and mobile-ad IDs, each written to its own list. The user's explicit yes — including that number — is the gate. Anything truncated or pruned is named before the run, never discovered after.
/watt:audience router, or a sibling leaf's offer (audience-generate at its landing, audience-analyze at its closing question) — with a built signal stack or a roster in session, or a re-supplied audience record.audience-activator — the confirmed audience (a signal stack's expression_string, or a roster's entity_ids_uri), ceiling, and writer-script path → pages pulled (or IDs enriched), transform run, file paths and row counts back. The platform's writer script (scripts/writers/<platform>.py) owns hashing and layout.audience-analyze — offered after delivery if the user hasn't had the read; another run for a different slice stays here.This surface says export, hashed — and explains the hashing once, plainly: "Export files carry hashed identifiers — your file holds digests, not raw emails, except the fields the platform reads in the clear." (Meta hashes everything but the mobile-ad ID; Google leaves country and zip in the clear and lists device IDs unhashed; Reddit hashes its two identifiers, emails and mobile-ad IDs.) It also names the expected audience size the honest way — the real-people range the platform will likely reach from this file, shown as a count, never a percentage or a guarantee (the full posture is below). Watt's word is audience size, never match rate — if the user arrives with "match rate," recognize it and answer in audience-size terms, never adopt it. The stack vocabulary (signals, must-haves, exclusions) carries over from generate; AND/OR/AND_NOT still never reach the user.
location: line carries the fence the run re-applies (none (national) means no filter — dropping the line would silently widen the export), its entity_type rides into the dispatch, and its reach is "measured then" until the run re-measures. A refresh-shaped re-supply ("re-run this export") is exactly this path — the materialization is the refresh — and after delivery, re-write the audience record per the record contract (context/record.md) with today's measured total against the header's original target and the · refreshed suffix (reach 26.1M (band 1M–5M) · refreshed), so the user walks away with the updated recipe alongside the export file.audience-generate-search; expand or overlay from audience-generate-list): an ID-only entity set already chosen, with classification columns that vary by strategy. Grouping gives ranked groups (group_label, cell_lift, cell_size, rank; roster_uri for the whole, an entity_ids_uri per group). Overlay gives a ranked list (overlay_score, signals_matched, rank — rows, not groups). Crossing and expand give an unordered membership set — no rank, no groups (crossing: entity_id + source_provenance; expand: entity_id + match confidence columns). Whichever it is, the entities are already chosen — there's no expression to materialize; the export shape is the choice (the confirmation, flow step 2). Take the record as given; never re-rank, re-group, or re-score it./watt:explore session's kept signals, or a lookalike pool) with an export intent. A pool has no expression yet — auto-compose it to the default stack first: pool-marked must-haves all-of, exclusions none-of, the rest any-of; if the pool carries no role markers, ask once ("any must-haves or must-have-nots, or export them all as one group?") before composing. A deterministic reading of the user's picks (the same operation as reconstructing a record's role groups), never a strategy compose — note plainly that a tuned build is audience-generate's lane if they want it. Measure the headcount once (count-only), then go to the confirmation with that real number.audience-generate step honestly.The preflight (step 1) precedes every path in — a built stack, a re-supplied record, a roster, or a pool — before any confirmation or number reaches the user.
The export runs through the Signal Graph's download/upload tools (generate_download_url, generate_upload_url). So before naming the platform menu or quoting a number, confirm those tools are present on the connection and the connector is authenticated — check that the tools are registered and available, not by firing a probe call with placeholder arguments (a missing-argument or validation error isn't a connection failure, and acting on it would push a connected user into the connect flow wrongly). This is cheap, and it spares the user from authorizing an export that then dies because the connector was never live.
What this catches is a missing or unauthenticated connector — caught up front, here. What it cannot catch is the code-execution network-egress block: those tools return a presigned URL successfully even when egress is denied, so that failure only surfaces later, when the activator fetches the artifact — and it's handled at pull time by its own entry in Failure modes, not here.
Resolve the platform's writer script first — scripts/writers/<platform>.py in this skill's own directory (today that's scripts/writers/meta.py, scripts/writers/google.py, and scripts/writers/reddit.py; if the runtime relocated the files, locate it before promising anything). The <platform>.py scripts present are the platform menu (a file whose name begins with _, like _common.py, is shared plumbing — never a platform): today that's Meta, Google, and Reddit, so the platform is the user's first pick — name the menu and let them choose before anything else. Run python3 <script> --list-identifiers and use its answer — for Meta and Google: email, phone, name, address, maid; for Reddit: email, maid — as the identifier list you confirm; the script is the source of truth, not memory.
Work out the ceiling honestly: the server's export budget caps a five-identifier pull at 600,000 rows per run — so the ceiling is the smallest of the audience's measured reach, the user's own number, and 600,000. If reach exceeds what one run can carry, that's said now ("your audience is 2.4M; one export run carries up to 600K of it").
A roster adds one choice — the export shape, and the roster's classification bounds what's offered:
roster_uri (capped at the run budget). Always available; for a membership-only roster (crossing's qualified set, expand's widest match — no rank, no groups) it's the shape — there's nothing to take a "top-N" of, and no groups to split.rank (grouping and overlay today) — never offered for an unranked membership set. For a grouped roster the slice is the top groups (e.g. "the top 5 groups, ~190K people" — the combined entity-ID set of those groups); for an overlay roster it's the top rows (e.g. "the top 500 by score"). One activator dispatch over the chosen set.group_label naming each (e.g. five files, one per metro). One activator dispatch per group's entity_ids_uri, each capped by the same 600K-per-run budget. Only when the roster has groups (the grouping objective).Name the shape choice in the confirmation alongside platform, scale, and identifiers — for one-file-per-group, the scale is per file and the file count is part of what the user authorizes.
Then the gate — landed per the render contract (context/visuals.md) — one decision, all the facts in it. State the chosen platform's actual transform; they differ, so confirm the right one. Each gate also carries the expected audience size — of the people in this run, roughly how many the platform will likely reach on its side — computed from the run's people ceiling by audience_size_range.py, this skill's own range script (python3 ${CLAUDE_PLUGIN_ROOT}/skills/audience-activate/scripts/audience_size_range.py <people_ceiling> → the rounded low–high count to quote; if the runtime relocated the files, locate it the same way as the writer scripts before promising anything). It's a platform-side estimate set in real people, never a percentage and never a guarantee — see expected audience size:
Meta — Exporting weekend hikers, reach 2.4M, this run pulls up to 600,000 people with email, phone, name, address, and mobile-ID identifiers, written into a Meta file (PII hashed; mobile-ad IDs raw, as Meta reads them in the clear). People with no email or phone are dropped (Meta has no way to reach them). Of these, your expected audience size on Meta is ~300K–480K people — a platform-side estimate, not a guarantee. Go?
Google — Exporting weekend hikers, reach 2.4M, this run pulls up to 600,000 people into a Google file (emails, phones, and names hashed; country and zip in the clear; mobile device IDs written to a separate unhashed list). People with no email, phone, or device ID are dropped (nothing left for Google to reach them by). Of these, your expected audience size on Google is ~300K–480K people — a platform-side estimate, not a guarantee. Go?
Reddit — Exporting weekend hikers, reach 2.4M, this run pulls up to 600,000 people into a Reddit file (emails and mobile-ad IDs hashed, each written to its own list). People with no email or device ID are dropped (nothing left for Reddit to reach them by). Of these, your expected audience size on Reddit is ~300K–480K people — a platform-side estimate, not a guarantee. Go?
An explicit yes including the number is the only thing that opens the gate. "Just do it" without the scale on screen is not a confirmation — put the numbers up and ask once.
Dispatch audience-activator with the confirmed platform, the confirmed ceiling, the location, the artifact's workflow_id, the script's absolute path, and the prune choice — plus the audience itself, in whichever form the input takes:
expression_string exactly as built.entity_ids_uri for the set the user confirmed, never an expression. The whole set: one dispatch over the full roster_uri (the shape for an unranked crossing set). Top-N by rank (ranked rosters only): one dispatch over the entity-ID set of the chosen top-N — the combined set of the chosen groups (grouped roster) or the top rows (overlay roster) — with the slice named as the activator's output_label (e.g. top_5_groups, top_500_by_score). One file per group (grouped rosters only): one dispatch per group's entity_ids_uri, passing that group's group_label as the activator's output_label — the label prefixes the file's name and rides back in the return's files entry, which is what anchors each delivered file to its group across the loop. Confirm the file count is within reason before looping, and narrate each. (Classification columns don't ride through enrichment — the writer reads identifier columns only; group identity travels as the dispatch's output_label, never as a column in the platform file.)Narrate its progress as it reports — pages pulled, transform running, counts moving. Track the dispatch as a session task; complete it on return.
Render the return:
meta_audience.csv whose rows can exceed persons (one row per identifier pair); Google writes two — google_audience.csv (one row per person) and google_audience_maid.csv (the device-ID list, a separate match path, so its count differs from the person file); Reddit writes two — reddit_email.csv and reddit_maid.csv, each a separate match path with its own count. Report every file, not just the first.audience_size_range.py on the actual exported people count — the post-prune people count, not file rows (Meta's rows can exceed persons) — python3 ${CLAUDE_PLUGIN_ROOT}/skills/audience-activate/scripts/audience_size_range.py <exported_people>. State it in real people, never a percentage: "Of the 600,000 people in your file, your expected audience size on the platform is ~300K–480K." Then the one-line posture, plainly: that linked share is the platform's own measure of who it already has on its side, not a measure of these people's realness or this audience's accuracy; every exported row is a real person, and one the platform doesn't reach just isn't on its side. If the user wants a larger reached audience, name the levers honestly: more identifier types per person, fresher identifiers, and meeting the platform's own list-size minimum. See expected audience size for the full posture and the lines we won't cross.workflow_id (the same export, reproducible) and the raw page URLs (1-hour traceability; they carry the unhashed identifier values if the user needs those — the platform file is the deliverable).Offer the natural next steps in one line: another run for a different slice, an audience-analyze read if they haven't had one, or done. Then record the run (silent plumbing — don't mention it):
STATE_DIR="${CLAUDE_PLUGIN_DATA:-${HOME}/.claude/plugins/data/watt}"
mkdir -p "$STATE_DIR"
cat > "$STATE_DIR/state.json" <<EOF
{
"version": 1,
"first_run_complete": true,
"completed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"last_workflow": "audience-activate"
}
EOF
When the file is uploaded, the platform reaches only the share of the people in it that it already has on its own side. It's always below the count we handed over, and what drives it sits largely outside Watt: the platform's own identity graph, the customer's ad account, how fresh the identifiers are, the channel. Watt's language for that reached share is the expected audience size — a real-people range, always. (The platform's dashboard calls the same thing a match rate and shows it as a percentage; if the user arrives with that word, answer in audience-size terms — recognize it, don't adopt it.) Hold one honest posture, every run:
scripts/audience_size_range.py, the single source of the band), so the read is "of these N people, your expected audience size is X–Y" — concrete, and clearly an estimate.workflow_id is the recovery.authenticate / complete_authentication tools, don't go diagnosing the connector or the MCP registry, and don't press on. Stop and come back to the user with the fix per the orientation's Getting connected note — the connect path, the setup docs (https://wattdata.ai/docs/get-started/quickstart), and the Claude organization guide to send their admin (https://wattdata.ai/docs/integrate/claude-organization), always.npx claudepluginhub wattdata/plugin --plugin wattProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.