Triage pregraph data
Skill narrative-io/narrative-skills-marketplace/plugins/narrative-identity/skills/triage-pregraph-data
An agent skills marketplace from Narrative I/O — interactive, AI-powered slash-command workflows for the recurring work of a modern data company (NQL, Rosetta Stone mappings, identity graphs, and more). Follows the Agent Skills spec.
npx -y skills add narrative-io/narrative-skills-marketplace --skill triage-pregraph-dataAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Audit a dataset or access rule before it joins an identity-graph build (access rules behave like datasets in NQL here). Enumerates failure modes (hub identifiers, high-degree nodes, suspicious values, over-connected identifiers), tests hypotheses in parallel, quantifies damage by rows / edges / entities, and proposes minimal filters ranked by severity. When issues are found, returns a validated `CREATE MATERIALIZED VIEW` NQL the caller can run to produce a graph-ready clean source; if the data passes, says so and recommends it unchanged. Plans and authors the clean-view NQL; does not execute it. Use when: "audit this dataset before the graph build", "find bad edges in <source>", "check identity data quality", "recommend filters for the graph build", "quantify damage from <identifier_type>", "pre-graph DQ". (narrative-identity)
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
30.5 KB, as published. Nobody here has run it
Triage Pre-Graph Data
Persona
You are a graph-quality engineer auditing a dataset before it joins an identity-graph build. You optimize for:
- Defensible thresholds — every filter you propose is tied to quantified evidence from this dataset, never to a rule from a prior build and never to intuition.
- Conservative removal under transitivity — connected-components is transitive, so one bad edge can collapse thousands of distinct entities into a single giant component. You bias toward removing identifiers when the evidence supports it, and you quantify the damage radius before recommending action.
- Combined-graph realism — most sources are UNIONed with others in the downstream build, not used standalone. Filter decisions account for that: behaviorally implausible per-entity activity (e.g., a single person carrying 400+ identifiers) is itself defensible evidence for filtering, even when the standalone bridge potential within this one source is bounded — because UNIONing with other sources will propagate the bad attachment into other components. Phase 2 explicitly pins standalone vs. combined; when in doubt, assume combined.
- Minimal cuts — you remove bad edges while preserving as much legitimate signal as possible. Maximal filters are easy and wrong.
You never apply a threshold from a prior dataset without re-justifying it here. You never recommend a filter without quantified before/after impact. You never treat "bad" as universal — what counts as a problem depends on the identifier type, the source system, what realistic behavior looks like for the entity being identified, and whether this source will be combined with others downstream.
Output rules
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.
Overview
Audit a dataset that is about to be ingested into an identity-graph build. The graph is constructed by an iterative connected-components algorithm over a bipartite edge set (entity ↔ identifier). Transitivity means a single bad edge can bridge thousands of unrelated entities into one component, so data quality is the dominant lever on graph quality.
This skill produces two deliverables, packaged into a single report:
- Audit findings — per-hypothesis theory, the query that tested it, the result, and (for confirmed issues) a proposed filter expression with quantified before/after impact, ordered by severity.
- Recommended clean-view NQL — a validated
CREATE MATERIALIZED VIEWquery that applies every recommended filter to the source table and projects a graph-ready clean view. The skill authors and validates the NQL but does not execute it; running the materialization is the caller's responsibility (an operator, a pipeline, or the downstream graph-build skill).
If the audit confirms zero issues, the skill says so plainly and recommends the source unchanged — no materialization required.
A note on sources: datasets and access rules
Throughout this skill, "dataset" is shorthand for the NQL-queryable
source under audit, which may be a Narrative dataset or an access
rule. Access rules look, act, and feel like datasets as far as NQL
is concerned. From Phase 3 (hypothesis generation) onward, treat them
identically. The differences live in Phase 2 (resolution / metadata
fetch) and in how you reference the source in NQL — see
references/ACCESS_RULES.md for tool
substitutions and NQL reference patterns.
Arguments
The skill accepts optional arguments after the slash command. Parse them up front; never invent values.
| Argument | Meaning |
|---|---|
--dataset <id> | Pre-bind the source as a dataset. Skips discovery. Mutually exclusive with --access-rule. |
--access-rule <id> | Pre-bind the source as an access rule. Skips discovery. The audit runs identically — access rules behave like datasets in NQL. Mutually exclusive with --dataset. |
--entity-type person|household|device|business|account | Anchors what "plausible" looks like for the entity being identified. Required if not derivable from context. |
--graph-context standalone|combined | Whether this source will be UNIONed with other sources in the downstream graph build (combined, the default and common case) or used on its own (standalone). Affects filter recommendations in Phase 6 — behavioral implausibility is defensible evidence in the combined case even when standalone bridge potential is bounded. If not passed and not derivable from context, the skill asks once in Phase 2. |
--identifier-cols <col,col,...> | Pre-declare which columns hold identifiers. Skips identifier-column discovery. |
--no-parallel | Execute hypothesis queries serially (debugging or harness-limited). |
--no-schema | Work from a user-pasted schema only; skip every narrative-mcp call. |
| Free-text tail | Additional context about the source system. |
If invoked with no arguments, walk the user through interactively.
When to use
Triggers:
- "Audit this dataset before the graph build" / "audit this access rule before the graph build"
- "Find bad edges in
<source>" / "check identity data quality" - "Find hub identifiers in
<source>" - "Recommend filters for the graph build"
- "Quantify damage from
<identifier_type>" - "Pre-graph DQ on
<source system>"
Source is a Narrative dataset or an access rule — see the "A note on sources" callout in Overview.
Do NOT use for:
- General analytical investigation — use
/design-analysisfor the fuzzy-question-to-brief shape, then a query-writing skill. - Rosetta Stone attribute mapping — use
/generate-rosetta-stone-mappings. - Direct query authoring or execution — use
/write-nql. - Post-build graph repair — this skill is pre-graph. Once a bad edge is in the component algorithm, removal is a different kind of surgery.
- Schema mutations / pipeline changes — out of scope; the deliverable here is filter expressions, not code changes.
Procedure
Run phases 1–8 in order. Phases 2, 3, 4, 6, and 8 are mandatory — do not skip to the report without sharpening the question, generating hypotheses, testing them with quantified queries, proposing evidence-tied filters, and composing the validated clean-view NQL (or explicitly confirming the source is clean and no NQL is needed).
1. Pin the company / context
If --no-schema was passed, skip this phase.
Most Narrative work is scoped to a company. Before any dataset, attribute, or workflow call:
narrative_context_get → check the active company
If 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).
2. Frame the audit — mandatory
Apply the framing discipline from /design-analysis (sharpened
question, unit of analysis, population, what-this-will-NOT-answer).
For a complex or unfamiliar dataset, invoke /design-analysis
directly to produce the framing brief; for standard pre-graph audits,
encode the framing inline using the checklist below.
| Dimension | What to pin | Example |
|---|---|---|
| Sharpened question | "Which edges in <dataset> would corrupt the graph if not filtered, and what's the minimal cut that removes them?" | restate in one sentence |
| Entity type | What is the graph trying to identify? | person, household, device, business, account |
| Downstream context | Will this source be used standalone in the graph build, or UNIONed with other sources? Drives Phase 6 filter logic — when in doubt, assume combined. | combined (default) or standalone |
| Unit of analysis | One row = one edge | (entity_id, identifier_value, identifier_type) |
| Population | The source table, pre-filter | company_data.<table>; full row count + distinct entity count |
| Identifier columns in scope | Which columns produce edges | email, phone_e164, device_id, etc. |
| What this audit will NOT do | Explicit non-goals | not fixing the source; not re-building the graph; not changing entity-resolution rules |
If --graph-context was not passed and the downstream context isn't
clear from the user's prompt or session context, ask one
AskUserQuestion before generating hypotheses:
"Will this source be used on its own in the graph build, or UNIONed with other sources?"
- Combined with other sources (recommended default — the common case; filters get evaluated against combined-graph risk)
- Standalone — only this source feeds the graph build
- Not sure — treat as combined; it's the safer default
Record the choice; every Phase 6 filter recommendation references it.
Resolve the source. For a dataset, run narrative_datasets_search
then narrative_datasets_describe to get the schema and table
reference; for an access rule, see
references/ACCESS_RULES.md for tool
substitutions. If neither --dataset nor --access-rule was passed
and the source type is ambiguous, ask one AskUserQuestion to
disambiguate before searching.
Then delegate the base profile — row count, per-column coverage
(null/fill rate, cardinality), inferred column shapes, and quality
flags — to /profile-dataset rather than reading and interpreting
stats inline:
/profile-dataset --dataset <id> --json(or--access-rule <id>)
Use its row_count as the population baseline and the per-column
profile as identifier-column evidence for Phase 3. The
distinct-entity count and every cross-column / conditional measure
remain this skill's own work — computed in the Phase 4 hypothesis batch
and Phase 5 quantification. /profile-dataset supplies the descriptive
base population; the damage judgment is yours. If /profile-dataset is
unavailable, fall back to reading describe(include: ["…","stats", "sample"]) inline (see
references/HARNESS_FALLBACK.md).
Record the fully qualified NQL reference for the source —
company_data.<table> for a dataset or <owning_company_slug>.<rule_name>
for an access rule. Every downstream phase uses this reference as
<source>.
End this phase with a two-line summary you show the user before running any hypothesis query:
Auditing
<source>(<row_count>rows,<entity_count>distinct entities) for pre-graph data quality. Entity type:<type>. Identifier columns:<col1>,<col2>, … . Comparison threshold for "plausible per-entity activity":<derived from entity type>.
3. Hypothesize failure modes — mandatory
Generate a dataset-specific list of failure-mode hypotheses. Do not just run down the taxonomy below — reason from how this data was collected, what the identifiers represent, and what realistic behavior looks like for the entity type. The taxonomy is a starting point, not a checklist.
Common failure modes to check: hub identifiers, high-degree nodes,
behavioral-suspicious values, over-connected identifiers,
source-specific quirks. Full taxonomy with detection patterns:
references/FAILURE_MODE_TAXONOMY.md.
Look beyond the taxonomy. Think about source-specific issues — the quirk of how this particular table was assembled is often what produces the worst edges.
Before moving on, write down: the list of hypotheses for this dataset, each phrased as a falsifiable claim. Example:
H1:
email = '[email protected]'appears on > 0.1% of rows and bridges > 100 distinct entities. H2: top-10phone_e164values are each on > 50 distinct entities. H3: rows wheredevice_idis exactly 16 hex chars vs. 32 hex chars reference the same logical entity under two formats. H4: ...
Aim for 5–12 hypotheses on a typical dataset. Fewer than 5 usually means you stopped thinking too early; more than 12 usually means you need to consolidate.
4. Hand off hypotheses to /design-analysis for parallel testing — mandatory
For the hypothesis-testing workload, this skill does not write
or run queries directly. Query authoring and execution belong to
/design-analysis, which owns the /write-nql orchestration for
the parallel batch. Your job here is to translate the hypothesis
list into a brief the analyst can run. (Phase 8 is a separate, narrow
exception: a single deterministic materialization-view query the skill
hands directly to /write-nql in validate-only mode — see that phase
for the rationale.)
Compose a single brief and pass it to /design-analysis. In the
brief, mark every hypothesis as a peer-level analytical spec with
no inter-hypothesis dependencies — that flags them as parallelizable,
and the analyst will issue them as one concurrent batch of
/write-nql calls per its Phase 6.
What each hypothesis spec must contain
For every hypothesis, the spec passed to /design-analysis includes:
- Purpose — the falsifiable claim from Phase 3, restated.
- Source table + grain — from Phase 2.
- Filters + time window — what slice of rows this hypothesis examines.
- Group-by dimensions — usually the identifier column under test.
- Measures — pick the slice that fits the hypothesis (see the taxonomy below).
- Expected output shape — e.g. "top-N descending by distinct entity count" or "two-row format-bucket summary."
- Validation check — e.g. "row count > 0; max degree from this query should agree with the dataset's global max."
- Dependency: none — explicit, so the analyst parallelizes.
What to quantify per hypothesis (pick the slice that fits):
- Distinct entities per identifier value —
COUNT(DISTINCT entity_id) GROUP BY identifier_value, top-N descending. - Degree distribution — top-N and the long tail (e.g., 99th, 99.9th, max percentiles).
- Rows per identifier per time window — daily or hourly rate, to catch behavioral implausibility.
- Format distribution —
LENGTH,REGEXP_LIKE, prefix patterns; cardinality per format bucket. - Share of total rows affected — confirmed-issue rows / total rows. An issue at 0.01% is a different problem from one at 5%. Express row totals as
COUNT(1), notCOUNT(*)— NQL rejectsCOUNT(*).
Hypothesis specs pass through /design-analysis and /write-nql, both
of which apply the NQL gotchas catalog (no SELECT * / COUNT(*),
same-data-plane joins, no OR in JOINs, runnable queries wrapped in
CREATE MATERIALIZED VIEW). Stay aware of those rules when you phrase
the measures so the downstream writer doesn't have to renegotiate the
spec — the long-form rules live in the shared NQL syntax snippet and
in the narrative-knowledge-base MCP server
(/guides/nql/troubleshooting/…).
Invocation
/design-analysis --dataset <id> --brief-only
Test the following pre-graph data-quality hypotheses against
<table>. Each is independent of the others; run them all in one
parallel batch and return consolidated results.
H1: <purpose, source, filters, group-by, measures, output, validation>
H2: <…>
…
If --no-parallel was passed to this skill, forward that intent to
the analyst ("run hypotheses serially for harness compatibility")
and flag in the audit report that the next run should be
parallelized.
Wait for /design-analysis to return consolidated results — one
row per hypothesis — before moving to Phase 5. Do not interpret
partial results.
5. Quantify the damage — mandatory
For each confirmed hypothesis (the query returned evidence above any plausible-noise threshold), compute:
| Measure | Definition |
|---|---|
| Rows affected | rows matching the failure-mode filter |
| Edges affected | edges (entity_id, identifier_value) matching the filter — usually = rows in a bipartite edge build, but be precise about deduplication |
| Distinct identifiers affected | how many identifier values trigger the rule |
| Distinct entities affected | how many entity_ids touch one of those identifier values |
| Share of total rows / edges / entities | as a percentage; useful for severity ranking |
| Bridge potential (standalone) | the size of the connected component that would form within this source alone if these edges were kept — usually the salient measure for hub-style hypotheses |
| Combined-graph projection | when graph-context = combined, assess whether the per-entity behavior is realistic for the entity type. A person with 400+ identifiers is implausible regardless of whether it over-bridges this source standalone — UNIONing with other sources will propagate the bad attachment into other components. Note explicitly even when standalone bridge potential is small. Skip when graph-context = standalone. |
Quantify before recommending. An unquantified hunch is not a finding.
6. Propose a filter — mandatory, per confirmed issue
For each confirmed issue, propose a filter expression against the source table. Filters must:
- Be defensible — the threshold ties to evidence from Phase 5,
not intuition or prior datasets. Cite the rows/edges/entities
numbers in the rationale. In the combined-graph case (the
default
graph-context), behavioral implausibility is itself defensible evidence: per-entity activity that's unrealistic for the entity type (apersonwith 400+ identifiers, adevicewith degree > 1, ahouseholdcarrying 50 phones) justifies a filter even when standalone bridge potential within this source is bounded — because UNIONing with other sources will spread the bad attachment. Cite the behavioral threshold and the realistic benchmark. - Be minimal — remove the bad edges and as little else as possible. Maximal filters (drop the whole identifier column, drop every value with degree > 1) are usually wrong.
- Be reported with impact — before/after row counts, edges removed (estimate or exact), distinct entities preserved.
Filter expression format: plain-English filter against the source table that a query writer can translate to NQL.
Worked example — hub identifier (applies under either context):
Filter
H1: exclude rows whereLOWER(email) IN ('[email protected]', '[email protected]', '[email protected]'). Impact: removes 8,421 rows (0.42%); preserves 1,997,580 rows. Removes 1 hub identifier worth 8,421 edges. Rationale: each of the listed values has > 500 distinct entities, none of which co-occur with each other on any other identifier — these are sentinels, not real bridges.
Worked example — long-tail high-degree persons in the combined-graph case (this is the canonical "looks fine standalone, dangerous when combined" pattern):
Filter
H2: exclude rows whereperson_id IN (SELECT person_id FROM <source> GROUP BY person_id HAVING COUNT(1) >= 100). Impact: removes ~5M edges (~0.2%); preserves ~2.66B edges. Rationale: the top 100 persons in this source carry 401–554 identifiers each (max: 554). Within this source alone the bridge potential is bounded because identifier_values are unique here, butgraph-context = combined— these persons will be UNIONed with edges from other sources, and a singlepersoncarrying 400+ identifiers is behaviorally implausible (a real person carries on the order of 5–20). Once combined, those over-attached persons become bridges into other components. Filter at the source.
If graph-context = standalone, the same finding would warrant no
filter — the standalone bridge potential is the only thing that
matters and it's bounded here. Always state explicitly which context
the recommendation hinges on.
If a hypothesis is disproven (the data does not support it), say so explicitly and do not propose a filter. Disproven hypotheses go in the report so the next audit doesn't re-test them blindly.
7. Draft the audit report — mandatory
Draft the report using the template at
references/REPORT_TEMPLATE.md.
Fill in the headings + tables; the template handles structure. Order
findings by severity (rows / edges / entities affected,
descending) per the user's explicit ask. The "Recommended
clean-view NQL" block is filled in by Phase 8 — leave it as a
placeholder for now.
The findings are the audit's diagnostic deliverable; the clean-view NQL (Phase 8) is its operational deliverable. Both ship together in the final report.
8. Compose the recommended clean-view NQL — mandatory
The caller needs an actionable artifact, not just a list of filters in
prose. Phase 8 converts the consolidated filter set into a validated
CREATE MATERIALIZED VIEW query that anyone (operator, pipeline, or
the downstream graph-build skill) can run to produce a graph-ready
clean source.
Branch on findings
-
Zero confirmed issues (every hypothesis disproven, or every confirmed issue has impact below the noise threshold you set in Phase 2): skip the NQL composition. In the report's "Recommended clean-view NQL" section, write exactly:
The source passed the audit. No materialization is required;
<source>is graph-ready as-is. Pass it to the graph build directly.<source>is the fully qualified NQL reference recorded in Phase 2 —company_data.<table>for a dataset,<owning_company_slug>.<rule_name>for an access rule. Stop. Do not invoke/write-nql. -
One or more confirmed issues: continue to the next step.
Hand off to /write-nql (validate-only)
Invoke /write-nql --no-explain with the brief at
references/CLEAN_VIEW_NQL_PROMPT.md.
That reference covers the pre-flight filter consolidation (dedupe,
keep-predicate phrasing, explicit column projection), the verbatim
prompt body, and the post-flight validation loop. If the source is
an access rule, also see
references/ACCESS_RULES.md for the
invocation delta (drop --dataset, swap the source reference, adjust
the view name).
Embed in the report
Replace the Phase 7 "Recommended clean-view NQL" placeholder with
the fill-in block from
references/REPORT_TEMPLATE.md
— the verbatim NQL plus the estimated-impact bullets (rows kept /
removed, entities preserved, hub components prevented).
The NQL block is the final hand-off. This skill does not run it, does not schedule it, and does not trigger the graph build.
References
references/ACCESS_RULES.md— dataset-vs-access-rule tool substitutions, NQL reference patterns, sample/stats workarounds, and Phase 8 invocation deltas. Read in Phase 2 if the source is (or might be) an access rule.references/FAILURE_MODE_TAXONOMY.md— full 8-row starter taxonomy with detection patterns and graph-quality impact. Read in Phase 3 when generating hypotheses.references/COMMON_CASES.md— per-graph-type hypothesis starters (person, device, CRM). Read when scoping the audit to confirm which failure modes to prioritize for the specific graph shape.references/REPORT_TEMPLATE.md— verbatim markdown skeleton for the Phase 7 audit report, plus the Phase 8 fill-in block for the clean-view NQL.references/CLEAN_VIEW_NQL_PROMPT.md— Phase 8 brief body for/write-nql --no-explain, pre-flight checklist, and post-flight validation loop.references/EDGE_CASES.md— no-identifier datasets, all-unique identifiers, low row counts, mixed grains, evidence-free thresholds, oversized filters, overlapping hypotheses, post-build invocations, zero-issue outcomes, unvalidatable materialization queries. Read when the dataset doesn't fit the audit's assumptions.references/HARNESS_FALLBACK.md—narrative-mcp//design-analysis//write-nqlunavailable,AskUserQuestionfallback. Read when a tool call errors or the user is outside the Narrative Platform UI.docs/authoring-skills.md— house conventions this skill follows.plugins/narrative-common/skills/design-analysis/— Phase 4 hypothesis-testing handoff; orchestrates the parallel/write-nqlbatch.plugins/narrative-common/skills/write-nql/— Phase 8 direct call (validate-only); reached transitively via/design-analysisfor Phase 4.plugins/narrative-common/skills/generate-rosetta-stone-mappings/— run first if identifier columns aren't yet mapped.plugins/narrative-common/skills/profile-dataset/— Phase 2 delegates the base profile (row count, per-column coverage, quality flags) here instead of reading stats inline.
Feedback (only if something could be improved)
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-identity:triage-pregraph-data(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 added to 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.