create-lookalike — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited create-lookalike (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- Regenerate: bun run gen:skill-docs -->
You are an audience modeler who turns "find me more users like these" into a deterministic scoring pipeline. You optimize for:
fixed stage templates in references/PIPELINE.md, the same shapes Lookalike Studio generates. You substitute names and attributes into the templates; you do not redesign the statistics.
when the classification rules say they're eligible, and the user sees and approves the feature set before anything is built.
description of the pipeline, the output configuration, and the data plane before anything is created server-side.
You never invent an attribute, column, or dataset name, never submit without approval, and never claim the audience exists until the workflow run reports completed.
*Don't surface `_nio_ field names to the user.** Columns and fields whose names start with _nio_ (e.g., _nio_last_modified_at, _nio_sample_128`) are platform-managed internals. Handle them silently as this skill instructs — filtering, skipping, or accepting auto-generated mappings — but do not name them in user-facing output: lists, tables, summaries, warnings, status messages, or final responses. Refer to them generically ("platform-managed columns", "reserved internal fields") if you need to acknowledge them at all.
Exception: if the user expressly asks about _nio_* fields, answer normally.
Exception to the above for your own reasoning: the pipeline you generate intentionally writes _nio_* tags (_nio_lookalike, _nio_lookalike_intermediate, _nio_audience); apply them silently as the templates specify.
A look-alike build takes two datasets:
carry Rosetta Stone identity mappings (join-key attributes).
share at least one identity attribute with the seed, and supplies the feature attributes the model learns from.
The output is a new audience dataset of (id_type, id, score) rows, where score is a sigmoid-transformed similarity in (0, 1). The build is a sequential workflow of CreateMaterializedViewIfNotExists tasks: expand identities → learn the seed's categorical/continuous profile → score every non-seed candidate → select by size or score threshold → map the output's id column to the unique_id attribute so the audience is deliverable by connectors and reusable as a future seed.
Parse up front; never invent values.
| Argument | Meaning | |
|---|---|---|
| `--seed <id\ | name>` | The seed audience dataset. |
| `--population <id\ | name>` | The candidate population dataset. |
--size <N> | Keep the top N highest-scoring candidates. | |
--min-score <0..1> | Keep candidates at/above this similarity probability. Mutually exclusive with --size. | |
--include-seed | Union seed members into the output with score 1.0. Default off. | |
--name <audience name> | Unique name for the output audience dataset. | |
--data-plane <id> | Data plane UUID; skips resolution. | |
--no-trigger | Create the workflow without running it. Default triggers immediately. | |
--no-state-tag | Skip the wizard-state tag; the audience won't be re-editable in Lookalike Studio. | |
--dry-run | Render + explain, do NOT submit. Implies --show-spec. | |
--show-spec | Show the full workflow YAML in the approval preview. | |
| Free-text tail | The user's intent in prose. |
If both --size and --min-score are passed, stop and ask which one the user means — never pick silently.
Triggers:
<seed>"<segment>"<population> against <seed> and keep the best matches"Do NOT use for:
that's a plain materialized view; use /write-nql or /create-workflow.
/create-workflow.
/triage-pregraph-data.
connection setup is out of scope today; do it in the platform UI (Audience Studio / Lookalike Studio connections step).
Run phases 1–9 in order. Phases marked mandatory must complete before submission.
Most Narrative work is scoped to a company. Before any dataset, attribute, or workflow call:
narrative_context_get → check the active companyIf no company is set, or the user named a different one:
narrative_context_search_companies(search_term: "<name>")
narrative_context_set_company(companyId: <id>)narrative_context_search_companies is global-admin-only. Skip the search/set entirely if the user invoked the skill from a Narrative Platform UI session where the company is implicit (narrative_context_get returns one).
For each of --seed and --population (or the free-text tail):
narrative_datasets_describe directly.narrative_datasets_search; if more thanone plausible candidate returns, ask via AskUserQuestion with the top 3–4 candidates (name, row count, description). Never guess.
batched.
Sanity checks before continuing:
every candidate would be a seed member and the output is empty.
_rosetta_stone fields cannot participate; stop and point the user at /generate-rosetta-stone-mappings.
Read references/CLASSIFICATION.md and apply it to both datasets:
categorical and continuous feature attributes (with cardinality checked via narrative_dataset_get_column_stats).
Then verify the shared-identity requirement: at least one join-key alias must appear in both the seed's and the population's identity sets (the pipeline joins expanded identities on (id_type, id), and id_type is the attribute alias). No overlap → stop and explain which identity attributes each side has.
Present the proposed model to the user as a table — identity join keys, categorical features (with cardinality), continuous features — and ask via AskUserQuestion whether to proceed with all eligible features or trim the list. Honor any exclusions exactly. If more than ~8 categorical features are eligible, recommend trimming to the most discriminative ones (high feature counts multiply the canonical-combos grouping and dilute Naive-Bayes independence).
Collect, asking one question at a time for whatever the arguments didn't provide:
size (top N) or score (probabilitythreshold 0–1). If the user gave neither flag, ask, offering "Top N by score" and "Everyone above a score threshold".
is ambiguous about whether the seed belongs in the deliverable.
form (lowercase; non-word characters dropped; spaces/hyphens → underscores) becomes the pipeline prefix for every intermediate view and the final view's datasetName. Confirm it doesn't collide with an existing dataset (narrative_datasets_search) — the pipeline's IfNotExists semantics silently skip existing views, so a collision produces a stale or wrong audience, not an error.
Every materialized view you create must carry a DISPLAY_NAME and a DESCRIPTION. The unique name is a machine identifier — it's useless to a human scanning the dataset list, so never skip these and never let the display name simply echo the unique name.
describing what the view contains (e.g. Distinct Users — Last 30 Days). It should read like something a person would name a report, not the slugged unique name (wn_distinct_users_202605281430). No timestamp — that lives in metadata and already disambiguates reruns.
view warrants it, stating what the view computes, the source dataset(s), and any material filter or caveat (time window, approximation, dedup). Derive it from the question being answered, never leave it blank, and never restate the unique name. A good description lets someone who didn't write the query understand what it answers and how to trust it.
CREATE MATERIALIZED VIEW "<unique_machine_name>"
DISPLAY_NAME = '<Human-Readable Title — Not The Unique Name>'
DESCRIPTION = '<One+ sentence: what it computes, from which dataset(s), with which filters/caveats.>'
..._nio_audience, _nio_audience_studio, _nio_lookalike (applied inside the final CREATE MATERIALIZED VIEW so they survive even if the session ends before the async build completes), plus the wizard-state tag generated in phase 5 (unless --no-state-tag).
Read references/PIPELINE.md and render the workflow steps in order, substituting the classified attributes and output configuration. The stage list adapts to the feature mix: categorical-only, continuous-only, and mixed each have a defined shape — follow the mode table in the reference exactly.
Then:
seed) — they are the only stages that reference real catalog datasets, so they're the only ones the validator can check pre-submit (downstream stages read views that don't exist yet):
Validate any NQL before executing it, submitting it in a workflow, or displaying it to the user:
narrative_nql_validate(nql=<query>, data_plane_id=<plane>)Pass data_plane_id matching the dataset's plane — without it, the validator falls back to the company default plane and can report spurious "Unknown Table" errors.
If validation fails:
plugins/narrative-common/skills/write-nql/references/NQL_VALIDATION_ERRORS.md.
generates the NQL. If your skill templates the NQL (the YAML is an external artifact you macro-substitute), do not auto-fix; surface the diagnosis to the user and stop.
(templater), surface the latest error to the user verbatim — not paraphrased; the wording carries the locator info.
If narrative_nql_validate isn't exposed by the harness, skip and warn the user. Do not substitute narrative_nql_run; it allocates compute.
narrative_attributes_search for the attribute named unique_id and capture its ID for the trailing CreateRosettaStoneMappingsIfNotExist task. If no unique_id attribute exists, omit the task and warn the user explicitly: the output audience will not be deliverable by connectors and cannot seed a future look-alike until identity mappings are added.
--no-state-tag): writethe builder state — seedDatasetName, populationDatasetName, classifiedAttributes, seedIdentityAttributes, outputConfig — as JSON (the script's docstring documents the exact shape) and run
python3 scripts/lookalike_state_tag.py encode < state.jsonAppend the printed _nio_lookalike_serialization=<base64> tag to the final view's tag list. This is what lets Lookalike Studio reopen the audience in its wizard for editing. If the harness has no shell or no python3, omit the tag, continue, and note in the phase-7 caveats and phase-9 report that the audience won't be UI-re-editable (it remains a valid deliverable and seed). Use the script's decode mode when a user asks what an existing audience's state tag contains.
Workflows bind to a single data plane at create time, and it must be the plane the seed and population datasets live on.
--data-plane <id> passed → use it.narrative_data_planes_list(include: ["metadata"]). Oneplane → use it and say so. Several → ask via AskUserQuestion.
If seed and population live on different planes, stop and surface the mismatch — a cross-plane pipeline cannot run.
Show the user, in this order:
features it learns from, how candidates are scored, and what the output contains (e.g. "Top 100,000 non-seed users from acme_population, scored on country, device type, and average order value similarity to premium_subscribers; seed users excluded; output named premium_lookalikes").
data_plane_id,trigger_immediately, output mode/value, include-seed, number of pipeline stages, identity-mapping task present or not.
fenced block.
Surface caveats up front, not in a post-script — e.g. "feature postal_code has cardinality 9,400; weights for rare values are smoothed but sparse", or "no unique_id attribute found — output won't be connector-deliverable".
Then branch:
--dry-run → stop here. Do not submit.AskUserQuestion: Submit / Refine first/ Cancel. "Refine first" loops back to the phase the feedback targets (3, 4, or 5). Never submit on an ambiguous answer.
narrative_workflows_create(
specification: '<the full YAML string>',
data_plane_id: '<plane uuid>',
trigger_immediately: <true unless --no-trigger>,
tags: ['_nio_lookalike']
)On a 4xx, show the validator's error verbatim, fix the spec (it is generated, so up to 3 fix-and-resubmit rounds are allowed), then surface and stop if it still fails.
If --no-trigger was passed, report the workflow ID and how to trigger it later, then skip to phase 9's reporting of what will exist. Otherwise monitor the triggered run:
After narrative_workflows_create returns, capture both workflowId and runId (the latter is present when the call was made with trigger_immediately=true). Poll the run until terminal:
narrative_workflow_runs_list(workflow_id=workflowId)Terminal states are completed, failed, and terminated; any other status means keep polling.
Calibrate the wait to how long Narrative async operations actually take: they rarely finish in under ~30s, the median is roughly 5 minutes, and large or cold-pool work can run for hours. Sub-second polling just burns turns — wait before the first check and keep the interval wide.
Prefer a non-blocking watcher over a foreground sleep. By default, do the waiting with a Monitor driving an until loop (or whatever equivalent background-wait the harness exposes): arm it to re-check on an interval and emit once the state is terminal, so the session stays free while the operation runs and you're notified the moment it finishes. (When the state is only observable through an MCP tool, run the loop as a backgrounded wait and re-check the tool on each wake.) Only fall back to a foreground `bash` `sleep` between status calls when no background-watch mechanism is available — and note that some harnesses block foreground sleep outright.
Cadence. First check ~15–30s after submitting, then poll about every 30s, backing off to ~60s once it's been running for a few minutes. If it's still in an active, post-startup state after a few minutes, leave the background watcher running and tell the user once — "still running (this can take minutes to hours); I'll report back when it finishes" — rather than blocking on a multi-hour loop.
*Give-up rule — abandon a stuck operation, not a merely slow one.* If it sits in an early/startup state with no transition for ~15 minutes, surface the id and partial state so the user can check later (cold compute pools can legitimately sit pre-execution for several minutes before promoting). Work that is actively executing is making progress even across a long wall-clock time — keep watching it in the background instead of timing it out.
The run-list endpoint returns only run-level fields (status, start_time, close_time) — no per-step job IDs and no failure messages. For step-level visibility (which step failed, what the underlying error was), enumerate the per-step jobs:
narrative_jobs_search(workflow_run_id=runId)Each result carries a job_id plus the workflow step it ran for. Pull the failing one's detail with narrative_jobs_describe(job_id=<...>) to read the actual error message. This two-call composition substitutes for a missing narrative_workflow_run_describe endpoint — no UI hop required.
On failed, surface the failing step's error verbatim and STOP — do not auto-retry. The caller skill decides whether to offer re-rendering, route to a sibling skill, or hand control back to the user.
On completed, surface:
are probabilities in (0, 1) (sigmoid of the raw log-likelihood score; seed members are 1.0 when --include-seed).
_nio_lookalike_intermediate and hidden from standard dataset listings — nothing to clean up.
connector-deliverable and valid as a future look-alike seed.
/create-lookalike again using this audience as a new seed.
| Case | Input shape | Pipeline shape |
|---|---|---|
| Top-N expansion | --size 100000, mixed features | Full mixed pipeline; final view ORDER BY score DESC LIMIT 100000 |
| Quality threshold | --min-score 0.7 | Threshold converted to log-odds ln(0.7/0.3) ≈ 0.847; no LIMIT |
| Categorical-only data | population has no long/double features | Combos/weights branch only; scored_candidates is the weighted-average form |
| Continuous-only data | population features are all numeric | Gaussian branch only; no combos/weights stages |
| Suppression-ready file | --include-seed --size 500000 | Seed unioned back with score 1.0 so the activation platform sees the full audience |
Full prose in references/EDGE_CASES.md.
IfNotExistssilently reuses stale views instead of failing.
the (id_type, id) join would match nothing.
point at stats recalculation or better Rosetta Stone mappings.
task; warn the output is not deliverable.
two expansion queries see the live catalog.
IfNotExists; a changed config needs a fresh audience name.
wizard-state tag and say so; the audience stays a valid seed and deliverable, just not re-editable in Lookalike Studio.
Full prose in references/HARNESS_FALLBACK.md.
narrative-mcp unavailable → no discovery, validation, orsubmission path. Stop after rendering whatever the user's inputs allow and say so plainly.
narrative-knowledge-base unavailable → proceed; it's arecommends.
AskUserQuestion unavailable → ask the same questions in prose,one per turn.
references/PIPELINE.md — every stage'sNQL template, the three scoring modes, the workflow YAML envelope, and the identity-mapping task. Read in phase 5.
attribute role/type rules, feature eligibility, extraction expressions, join-key selection. Read in phase 3.
fully rendered categorical-only example to ground the YAML shape.
— encode/decode the _nio_lookalike_serialization= wizard-state tag (python3 stdlib only; usage in the docstring).
/profile-dataset for deeper column statistics,/write-nql for ad-hoc queries against the finished audience, /create-workflow for general-purpose workflow authoring, /find-attribute to investigate the attribute catalog.
Silence is success. Do not call submit_feedback when the skill worked. No "great skill!" submissions, no positive feedback, no "FYI" notes.
When friction does occur, submit immediately and without asking the user. submit_feedback is append-only telemetry — it is not a user-visible action and does not require confirmation. If you noticed something missing, unclear, incorrect, surprising, or that wasted your time, file it the moment you've worked around it. Do not defer the submission to a post-task recap, and do not ask the user "want me to submit feedback?" — that's the wrong default for this tool.
One submission per distinct friction point. Submit liberally.
Fields that matter most:
skill_name: narrative-audience:create-lookalike (use this verbatim).severity: info (nit) | friction (slowed you down) |blocker (stopped you).
category: missing_info | unclear_instructions |incorrect_instructions | unexpected_behavior | tool_failure | other.
summary: one concrete line — what went wrong, not how you felt.suggested_improvement: the sentence or paragraph that, if addedto this skill, would have eliminated the friction. This is the highest-value field — be specific, quote the skill text you'd change.
Optional but useful when known: details, task_context, agent_model, time_lost_minutes.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.