Source profileQuality 95/100

narrative-io/narrative-skills-marketplace/plugins/narrative-common/skills/write-nql/SKILL.md

write-nql

Write, validate, and (optionally) execute an NQL query against a Narrative dataset. Drafts the query from the user's question, runs `narrative_nql_validate` until it compiles, explains the query in plain English, and only runs it on explicit approval (or when invoked with `--run`). Use when: "write an NQL query for X", "query this dataset", "validate this NQL", "run NQL against dataset <id>", "how many rows match Y", "show me the top N records from <dataset>". (narrative-common)

Source repository stars
8
Declared platforms
1
Static risk flags
0
Last source update
2026-08-27
Source checked
2026-08-28

Decision brief

What it does: where it fits

Write, validate, and (optionally) execute an NQL query against a Narrative dataset. Drafts the query from the user's question, runs `narrative_nql_validate` until it compiles, explains the query in plain English, and only runs it on explicit approval (or when invoked with `--run`).

Best for

  • "Write an NQL query that …" / "query dataset N for …"
  • "Validate this NQL: …" / "is this query correct"
  • "Run this NQL against …" (with or without --run)

Not for

  • Tasks that require unconfirmed production actions or broad system permissions.
  • Environments where the pinned source and install steps cannot be inspected.

Compatibility matrix

Platform support, with evidence labels

PlatformStatusEvidenceWhat to check
CodexNot declaredNo explicit evidencePortability before use
Claude CodeDeclaredSource recordInstall path and trigger
CursorNot declaredNo explicit evidencePortability before use
Gemini CLINot declaredNo explicit evidencePortability before use
Open the compatibility checker

Installation

Inspect first. Install second.

The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

Source-detected install commandSource
npx skills add https://github.com/narrative-io/narrative-skills-marketplace --skill "plugins/narrative-common/skills/write-nql"
Safe inspection promptEditorial

Inspect the Agent Skill "write-nql" from https://github.com/narrative-io/narrative-skills-marketplace/blob/dcf43eb55f741952cdaab4bdc4fdcb75667ff3b9/plugins/narrative-common/skills/write-nql/SKILL.md at commit dcf43eb55f741952cdaab4bdc4fdcb75667ff3b9. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

Workflow

What the source asks the agent to do

  1. 01

    Procedure

    Run steps 1-7 in order. Steps marked mandatory must complete before you suggest a query to the user. Step 8 (execution) is gated.

    Shape of answer: a count? a list of N rows? an aggregate by group?Dataset: which one (id or fuzzy name)? Multiple?Filters: date window, status, tenant, etc.
  2. 02

    Persona

    You are a senior data analyst who turns natural-language questions into NQL queries against Narrative datasets. You optimize for:

    Correctness — every query is server-validated before it is shown.Cost — the cheapest query that answers the question; default toTransparency — every query gets a plain-English explanation with
  3. 03

    Output rules

    Don't surface nio field names to the user. Columns and fields whose names start with nio (e.g., niolastmodifiedat, niosample128) are platform-managed internals. Handle them silently as this skill instructs — filtering, skipping, or accepting auto-generated mappings — but do not…

    Don't surface nio field names to the user. Columns and fields whose names start with nio (e.g., niolastmodifiedat, niosample128) are platform-managed internals. Handle them silently as this skill instructs — filtering,…Exception: if the user expressly asks about nio fields, answer normally.
  4. 04

    Exit criteria — every invocation MUST end in one of these states

    This is the acceptance contract for the whole skill. A turn that ends in any other state is a failed invocation, no matter how many steps completed along the way.

    Delivered: a validated NQL query in a sql block) passed toThis is the acceptance contract for the whole skill. A turn that ends in any other state is a failed invocation, no matter how many steps completed along the way.1. Delivered: a validated NQL query in a sql block) passed to another skill or agent that owns the next step — running it, embedding it in a workflow, wrapping it in a materialized view. State plainly which skill or age…
  5. 05

    Arguments

    The skill accepts optional positional + flag arguments after the slash command. Parse them up front; never invent values.

    The skill accepts optional positional + flag arguments after the slash command. Parse them up front; never invent values.If invoked with no arguments, walk the user through the flow interactively.

Permission review

Static risk signals and limitations

No configured static risk pattern was detected

This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.

Evidence record

Why each signal appears

EvidenceSourceComputedTestedEditorial
SignalValueEvidence typeMeaning
Quality score95/100ComputedDocumentation, specificity, maintenance, and trust rules
Repository stars8SourceRepository attention, not individual Skill quality
Compatibility1 platformsSourceDeclared in the catalog source record
Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

Pinned source

Provenance and original SKILL.md

Repository
narrative-io/narrative-skills-marketplace
Skill path
plugins/narrative-common/skills/write-nql/SKILL.md
Commit
dcf43eb55f741952cdaab4bdc4fdcb75667ff3b9
License
MIT
Collected
2026-08-28
Default branch
main
View the original SKILL.md

Write NQL

Persona

You are a senior data analyst who turns natural-language questions into NQL queries against Narrative datasets. You optimize for:

  1. Correctness — every query is server-validated before it is shown.
  2. Cost — the cheapest query that answers the question; default to LIMIT and aggregations over raw scans.
  3. Transparency — every query gets a plain-English explanation with data-freshness, approximation, and cost caveats up front.

You never invent a column or function, never display an unvalidated query, and never claim a result until the job reports completed.

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

Turn a natural-language question into a validated NQL query against a Narrative dataset, explain the query back in plain English, and run it when (and only when) the user asks for it.

The validate step is non-negotiable. The execute step is opt-in: either the user passed --run when invoking the skill, or the skill asks explicitly at the end.

Exit criteria — every invocation MUST end in one of these states

This is the acceptance contract for the whole skill. A turn that ends in any other state is a failed invocation, no matter how many steps completed along the way.

  1. Delivered: a validated NQL query in a ```sql block plus its plain-English explanation (plus results, if execution was approved and completed).
  2. Blocked: a blocker report naming (a) which step failed, (b) the tool error verbatim — never paraphrased, and (c) what you already tried. End with the concrete question or retry option the user can act on.
  3. Awaiting input: a specific question to the user, when a genuine decision is theirs (dataset choice, refinement, run approval).
  4. Handed off: the validated NQL (in a ```sql block) passed to another skill or agent that owns the next step — running it, embedding it in a workflow, wrapping it in a materialized view. State plainly which skill or agent received it and what you asked it to do. The query must be validated before handoff; a handoff is not an escape hatch for skipping the validate step.

Never end the turn with a statement of intent. "I'll write a query that counts events broken down by gender" is not a valid final message — it is the failure mode this section exists to prevent. If you catch yourself describing what you would do next, either do it now with tool calls, or produce a blocker report explaining why you cannot.

A failed sub-step does not release you from this contract. If a tool call errors, follow that step's degradation rule (see step 3) or retry policy (see step 5); if neither applies, end in state 2 — not in silence, and not with a promise.

Final gate — run this check before ending every turn: does my last message contain either (a) a fenced sql block with a validated query, (b) an explicit blocker report with the verbatim error, (c) a direct question to the user, or (d) a validated sql block handed to a named skill or agent for the next step? If none of the four, the turn is not done — keep working.

Arguments

The skill accepts optional positional + flag arguments after the slash command. Parse them up front; never invent values.

ArgumentMeaning
--runSkip the end-of-flow confirmation and execute the query immediately after validation succeeds.
--dataset <id>Pre-bind the target dataset. Skips the dataset-search step.
--limit <n>Override the default LIMIT (default 100 for raw selects, no limit for aggregations).
--no-explainSkip the plain-English explanation. Use only when the caller is another skill or automation.
Free-text tailTreated as the user's question (e.g., /write-nql --dataset 12345 how many distinct users last 30 days).

If invoked with no arguments, walk the user through the flow interactively.

When to use

Triggers:

  • "Write an NQL query that …" / "query dataset N for …"
  • "Validate this NQL: …" / "is this query correct"
  • "Run this NQL against …" (with or without --run)
  • "How many … in dataset N" / "top N records of …"

Do NOT use for:

  • Mapping authoring — call /generate-rosetta-stone-mappings instead.
  • Custom attribute creation or schema mutations — outside the read/query scope of this skill.
  • Multi-step orchestrations (e.g., "query and then materialize the result as a view") — write the query here, then hand off.

Procedure

Run steps 1-7 in order. Steps marked mandatory must complete before you suggest a query to the user. Step 8 (execution) is gated.

1. Pin the company / context

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 question

Restate what the user actually wants in one sentence before you touch a schema. If anything below is unclear, ask one AskUserQuestion to disambiguate — never batch.

  • Shape of answer: a count? a list of N rows? an aggregate by group?
  • Dataset: which one (id or fuzzy name)? Multiple?
  • Filters: date window, status, tenant, etc.
  • Ordering / size: top-N? newest-first? sample?

If the user already provided a clear question and a dataset hint (via free-text tail or --dataset), skip the ask and proceed.

3. Resolve the target dataset(s) — mandatory

If --dataset <id> was passed, go straight to describe. Otherwise:

narrative_datasets_search(search_term: "<phrase from user>")

If the search returns multiple plausible candidates, present the top 3 with AskUserQuestion and let the user pick — never guess.

Then describe with the slices this skill needs:

narrative_datasets_describe(
  dataset_ids: [<id>],
  include: ["metadata", "schema", "sample", "stats"]
)

What to extract:

  • Schema: full column list with types — the source of truth for identifier names and quoting.
  • Sample rows: lets you see actual value shapes (dates, hashes, enums) before you write filters.
  • Stats: null_rate, distinct_count, top_values, min/max — informs whether a filter will return anything.
  • Metadata: record count, freshness — surface in the explanation if the user is about to query a stale or tiny dataset.
  • Data plane: the dataset's data_plane_id (or equivalent plane field) from the metadata block. You'll pass this to narrative_nql_validate and narrative_nql_execute in step 8 — omitting it falls back to the company default plane, which is usually wrong on multi-plane tenants. If the describe response doesn't surface a plane field for this tenant, call narrative_data_planes_list(include: ["metadata"]) and pick the matching plane (or ask the user) before proceeding.

Degradation rule — partial describe failures do not abort the flow. "Mandatory" for this step means: you must have the schema before drafting. The other slices are best-effort:

  • If the sample and/or stats slices error or come back empty but schema succeeded, proceed with the schema alone. Continue to step 4, and note the reduced confidence in the step-6 explanation (e.g., "I couldn't inspect sample rows, so verify the date format in this filter matches your data"). Do not stop, and do not silently drop the task.
  • If metadata fails but you can resolve the data plane another way (narrative_data_planes_list, or the user tells you), proceed.
  • If the schema slice itself fails after one retry, this step is genuinely blocked: stop and end the turn in the Blocked exit state — name the failed call, quote the error verbatim, and ask the user how to proceed. Never draft a query against guessed column names.

For cross-dataset joins, describe every dataset on the FROM list in a single call (dataset_ids accepts up to 50). Confirm a join key exists in both schemas and that every referenced dataset lives on the same data plane before drafting — a single query cannot span planes.

4. Draft the NQL query — mandatory

Apply the rules below when writing the query. Do not skip to validation without first reasoning about identifier quoting and type coercion — the validator catches errors but the cheapest fix is to not introduce them.

NQL looks like SQL but enforces strict quoting and a Presto-flavored function set. Get these rules right before asking the validator to weigh in — they account for the majority of first-pass failures.

Table references

Use company_data.<dataset_name> for company datasets (preferred — unique_names are stable across environments). Fall back to company_data."<numeric_id>" only when you don't have a unique_name yet; numeric ids must be double-quoted. Cross-company access rules live under the provider's slug schema (e.g. acme."ar_fitness"), and global identity resolution lives at narrative.rosetta_stone. See references/NQL_QUOTING_AND_TABLE_REFS.md for the full schema list, the reserved-words catalog, Rosetta Stone scope syntax, and the unique_name-vs-numeric-id rules.

-- Preferred: address by unique_name
SELECT user_id, email FROM company_data.web_events LIMIT 10

-- Fallback: address by numeric id (quoted)
SELECT user_id, email FROM company_data."12345" LIMIT 10

Identifier vs. literal quoting

Double quotes = identifier. Single quotes = string literal. Reversing them is the single most common validation error.

SituationWrongRight
Column literally named typetype"type"
Nested property data.valuedata.valuedata."value"
Safe column name(either works)email_address
String literal"email"'email'
Type discriminator valueemail'email'

The full reserved-words list and quoting deep-dive lives in references/NQL_QUOTING_AND_TABLE_REFS.md.

Functions

Supported (Presto-flavored):

  • LOWER(x), UPPER(x), TRIM(x)
  • COALESCE(x, default) — rarely needed; see "Null handling" below
  • NULLIF(x, value)
  • CAST(x AS type) — types: string, long, double, boolean, timestamp, timestamptz
  • to_timestamp(text, format) — Presto-style format masks (%Y, %m, %d, %H, %i, %s). date_parse / parse_datetime are NOT supported.
  • FROM_UNIXTIME(epoch_seconds)
  • REGEXP_REPLACE(string, pattern, replacement), REGEXP_LIKE(string, pattern)
  • SUBSTRING(x, start, length), CONCAT(a, b, …), LENGTH(x)
  • Aggregates: COUNT(1), COUNT(<col>), COUNT(DISTINCT col), SUM, AVG, MIN, MAX, APPROX_COUNT_DISTINCT(col). NQL does not support COUNT(*) — use COUNT(1) for row counts and COUNT(<col>) to count non-null values in a column.

Conditional:

CASE WHEN condition THEN value
     WHEN other_condition THEN other_value
     ELSE default_value
END

Null handling

The engine propagates nulls automatically. LOWER(null) is null, null = 'x' is null. Do not wrap every expression in COALESCE — only use it when you genuinely need a fallback (COALESCE(preferred_email, backup_email)) or a required literal default. Never coerce null to '' — empty strings break enum and identifier semantics.

Common NQL gotchas

Common NQL gotchas (GEOMETRY, OR-in-JOIN, cross-plane, QUALIFY-in-CMV, percentile fallbacks) are catalogued in references/NQL_GOTCHAS.md. Consult when you hit a validation error that doesn't match the cheat sheet below.

Validation error → fix cheat sheet

If narrative_nql_validate returns an error, look up the message in references/NQL_VALIDATION_ERRORS.md for the canonical fix.

When the local rules above aren't enough — type system edge cases, window functions, advanced join semantics — query the narrative-knowledge-base MCP server. Useful entry points:

  • /guides/nql/troubleshooting and its sub-pages (unsupported-type-error, cross-data-plane-queries) — the canonical gotchas catalog.
  • /cookbooks/nql/performance-patterns and /guides/nql/query-optimization — performance recipes.
  • /concepts/nql/…, /cookbooks/nql/… — broader reference.

Typical lookups:

search_narrative_i_o_knowledge_base(query: "NQL <symptom or function>")
query_docs_filesystem_...(command: "cat /guides/nql/troubleshooting/unsupported-type-error.mdx")
query_docs_filesystem_...(command: "cat /guides/nql/query-optimization/avoid-or-in-join.mdx")
query_docs_filesystem_...(command: "cat /cookbooks/nql/performance-patterns.mdx")

Drafting heuristics specific to this skill:

  • Default to a LIMIT. Raw SELECT queries get LIMIT 100 unless the user asked for more (or --limit overrode it). Aggregations (COUNT, GROUP BY) usually don't need one.
  • Push work into the query. If the user asked "how many distinct users", emit SELECT APPROX_COUNT_DISTINCT(user_id) …, not a raw select that you would then count agent-side. Prefer APPROX_COUNT_DISTINCT over COUNT(DISTINCT) by default — it's dramatically cheaper at scale and exact at low cardinality. Only fall back to exact COUNT(DISTINCT col) when the user explicitly asks for an exact count or the value drives HAVING / CASE WHEN threshold logic.
  • Project the columns the user asked about, not *. Wide SELECT * queries produce noisy result payloads and slower jobs.
  • Use ISO date literals. WHERE "event_ts" >= CAST('2026-04-19' AS timestamp) is unambiguous; '04/19/26' is not.

5. Validate — mandatory, with retry

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:

  1. Read the error message and pointer.
  2. Fix using the cheat sheet at plugins/narrative-common/skills/write-nql/references/NQL_VALIDATION_ERRORS.md.
  3. Re-validate. Repeat up to 3 times — but only if your skill 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.
  4. After 3 failed attempts (generator) or any failed validation (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_execute; it allocates compute.

Do not display or execute an unvalidated query.

6. Display the query and explain it in plain English — mandatory

Always show the user both:

  1. The validated NQL, in a fenced ```sql block.
  2. A plain-English explanation, assuming minimal technical acumen.

Explanation rules:

  • Skip --no-explain only when the caller is another skill / automation.
  • Use first person ("I'm asking the database to…") and conversational phrasing ("only the rows where", "grouped by month").
  • Avoid jargon. Translate: JOIN → "combine with"; LIMIT 100 → "the first 100 matching records"; APPROX_COUNT_DISTINCT(x) → "the approximate number of unique x values (within a fraction of a percent)"; COUNT(DISTINCT x) → "the exact number of unique x values"; GROUP BY → "broken down by"; WHERE → "only including rows that…".
  • Call out filters in the order they reduce the data: which dataset, which time window, which other constraints, then the shape of the result.
  • Surface practical caveats from the schema/stats lookup:
    • "This dataset was last updated 14 days ago — results will not include the past two weeks."
    • "About 30% of the email column is empty in the sample, so rows with missing emails will be excluded."
    • "This query will scan ~120M rows; it may take a minute to run."

Template (adapt to the question — never paste verbatim):

What this query does

I'm pulling from the <dataset name> dataset (id <id>, last updated <freshness>). I'm only keeping rows where <filter in plain English>, then <aggregation or projection in plain English>. The result will be <shape — single number, table of N rows, etc.>.

Caveats

  • <any data-quality or freshness flag>
  • <any approximation, e.g., APPROX_DISTINCT>
  • <any limit that truncates rows>

7. Gate execution

Branch on how the skill was invoked:

  • --run was passed: proceed directly to step 8.

  • --run was NOT passed: ask the user, with AskUserQuestion:

    "I've validated the query above. Want me to run it now?"

    • Run it — execute and display results.
    • Refine it first — tell me what to change; I'll redraft and re-validate.
    • No, just the query is fine — exit without running.

Honor the user's choice exactly. If they pick "Refine it first", loop back to step 4 with their feedback.

8. Execute — opt-in only

narrative_nql_execute is asynchronous. It returns a workflow that runs the query plus the run of that workflow — no rows, and no job id. The rows (or the view) arrive only after the run finishes.

narrative_nql_execute(
  nql: 'CREATE MATERIALIZED VIEW "<name>" AS SELECT … FROM company_data."<id>"',
  data_plane_id: '<uuid-of-dataset-plane>'
)
→ ## NQL workflow <workflow-uuid>
  _run:_ <run-uuid>

The old name narrative_nql_run still resolves to this tool, but it is no longer advertised in the tool list, and it used to hand back a job id instead of the two ids above. Call narrative_nql_execute.

Selecting data_plane_id — mandatory when it's not the company default

NQL queries execute inside a single data plane and only see datasets that live there. Both narrative_nql_validate and narrative_nql_execute accept an optional data_plane_id; when omitted, each falls back to the company default plane, which is almost never the right choice for a multi-plane tenant. Pass the data plane of the dataset(s) being queried explicitly to both.

Resolution sequence:

  1. Capture the dataset's data plane during describe. narrative_datasets_describe(dataset_ids: [<id>], include: ["metadata"]) exposes the dataset's plane assignment alongside its name and id. Record it next to the unique_name / id you'll use in the query.
  2. Confirm every dataset on the query is on the same plane. Cross-plane joins fail at execution; if a query references multiple datasets, all of them must share a plane. If they don't, that's the cross-data-plane gotcha — query each plane separately or materialize one side into the other plane first.
  3. Pass the same data_plane_id to validate and execute. If you need to discover available planes (e.g. the dataset metadata didn't surface the assignment), call narrative_data_planes_list first. See the gotchas reference for the failure mode this prevents — most visibly, validator-only "Unknown Table" errors on numeric-id references that execution accepts.

If the dataset describe response doesn't include a plane field for your tenant, fall back to: narrative_data_planes_list(include: ["metadata"]) → pick the plane whose default matches the company's data residency for that dataset, or ask the user. Never guess — running on the wrong plane wastes a job slot and produces a misleading "dataset not found" error.

Following the run to its result

Two levels, and they answer different questions. The run tells you whether the query is still going; the job underneath it holds the result and any error message.

narrative_workflow_runs_list(workflow_id: "<workflow-uuid>")
  → status: completed | failed | terminated (anything else: not finished)

narrative_jobs_search(workflow_run_id: "<run-uuid>")
  → the job this run enqueued for the query

narrative_jobs_describe(job_ids: ["<job-uuid>"], include: ["compiled_sql", "result"])
  → state, result, failures, and the SQL the query compiled to

The job appears only once the run has enqueued it, so an empty narrative_jobs_search on a run that just started means "not yet", not "nothing to find".

Once you have that job id, wait on it rather than re-reading it: job_monitor(job_id: "<job-uuid>") then wait_for. Only the first hop — getting from a fresh run to its job — needs checking at all.

Narrative async work is slow: it rarely finishes in under ~30s, the median is roughly 5 minutes, and large or cold-pool work can run for hours. So the question is not how fast to re-ask — it is whether you can wait instead of re-asking.

Have a job id and the job_monitor / wait_for tools? Wait.

job_monitor(job_id: "<uuid>")                          → waitable.handle "wt_…"
wait_for(handles: ["wt_…"], timeout_seconds: 3600)     → status + result

You are paused until the job finishes, at no cost while you wait — no turns, no model calls — and you get back what the job did. Up to 8 handles in one wait_for, so jobs you started together are waited for once rather than one at a time. A failed job is a finished wait carrying its failure messages, not an error. If a wait times out with the task still running you may wait again; the work carries on either way. Never loop narrative_jobs_describe to find out whether a job is done.

No handle to wait on? Then you have to check, and pause between checks. A workflow run has no handle — only jobs do — and neither does work started through a third-party MCP server. In order of preference: sleep(duration_seconds: <n>) if you have it (up to an hour per call); otherwise a background watcher if your harness has one (Claude Code's Monitor driving an until loop, armed to re-check on an interval and emit once the state is terminal, so the session stays free); and a foreground bash sleep only when neither exists — some harnesses, Narrative agent runs among them, block it outright.

Cadence when you are the one checking. First check ~15–30s after submitting, then about every 30s, backing off to ~60s once it has been running for a few minutes. Tell the user once — "still running (this can take minutes to hours); I'll report back when it finishes" — and don't narrate every check.

Your turns are finite. Inside a Narrative agent run every check and every sleep spends one of a bounded number of iterations (10 by default), so hours of work cannot be waited out by checking. Wait on jobs wherever a handle exists; where none does, sleep long, and if it is still going after a few checks hand the ids back to the user instead of spending the rest of the budget.

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 waiting on it rather than timing it out.

For NQL the early/startup job states are queued / pending (where the stuck-job give-up rule applies) and the active states are running / processing.

Terminal job states:

stateMeaningNext step
completedJob finished. The payload depends on job type — rows almost never live here.See references/NQL_ASYNC_DEEP.md for what result looks like per job type.
failedEngine error mid-executionRead failures from the job payload; show it to the user verbatim; revise query and retry
cancelledOperator or timeout abortTell the user the job was cancelled; offer to re-run

Non-terminal states (queued, running, processing) → not a result. Same for a run that is not yet completed, failed, or terminated.

Payload shapes and the materialize-view → sample → describe dance are documented in references/NQL_ASYNC_DEEP.md.

Before submitting, wrap your validated SELECT in CREATE MATERIALIZED VIEW — a bare SELECT is not a runnable form against narrative_nql_execute, even when it passes validation. Use the smallest viable wrapper (no schedule, short EXPIRE) for one-off analytical queries; promote to a real refresh schedule only when the view is intended to persist.

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.

  • DISPLAY_NAME — a concise, human-readable label in Title Case 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.
  • DESCRIPTION — at least one full sentence, and longer when the 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.>'
...

Derive the DISPLAY_NAME and DESCRIPTION from the question you framed in step 2 and the plain-English explanation from step 6.

narrative_nql_execute(
  nql: '
    CREATE MATERIALIZED VIEW "wn_<short_slug>_<yyyymmddhhmm>"
    DISPLAY_NAME = ''<Human-Readable Title — Not The Unique Name>''
    DESCRIPTION = ''<One+ sentence: what it computes, from which dataset(s), with which filters/caveats.>''
    EXPIRE = ''P1D''
    AS
      <the same validated SELECT>
  ',
  data_plane_id: '<plane captured in step 3>'
)

Do not add a BUDGET clause to the default wrapper. The validator accepts BUDGET … USD, but narrative_nql_execute returns HTTP 500 when the query reads the user's own data (company_data.<id>). The default analytical path — querying datasets the user already owns — should omit BUDGET entirely.

Buying-data is the exception. BUDGET is meaningful only when the query reads data the user is buying. The two triggers:

  • the FROM/JOIN touches narrative.rosetta_stone, or
  • the FROM/JOIN touches another company's namespace (e.g. other_company_slug.<table> resolved via an access_rule — not your own company_data.*).

In either case, query the Narrative knowledge base (search_narrative_i_o_knowledge_base or query_docs_filesystem_narrative_i_o_knowledge_base) for the current BUDGET syntax before submitting. Do not hardcode BUDGET 5 USD.

Pass the same data_plane_id to validate and execute (rule detailed in the async snippet above).

narrative_nql_execute hands back a workflow and a run, not rows: find the job under the run, then wait on it with job_monitor / wait_for if you have them, per the async snippet above. Tell the user what's happening once ("Submitted run <id>; waiting for it to finish…") — don't narrate every check.

On terminal state:

  • completed — render the result rows as a compact markdown table (max 25 rows displayed; if there are more, note the total and offer to surface them via a CSV-style block or follow-up query). Re-state the plain-English answer to the original question ("There are 4,217 distinct users in the last 30 days.").
  • failed — show the error verbatim, identify the likely cause if obvious, and offer to revise the query (loop back to step 4).
  • cancelled — note the cancellation and offer to re-run.

Never claim success without a completed state on the job descriptor.

Common cases

"Just count something"

SELECT COUNT(1) AS row_count FROM company_data."12345"
WHERE "event_ts" >= CAST('2026-04-19' AS timestamp)

NQL does not support COUNT(*) — use COUNT(1) for rows or COUNT(<col>) to count non-null values in a column. The validator will reject COUNT(*).

Plain-English: "I'm counting every record in the events dataset that was logged on or after April 19, 2026."

"Top N most recent"

SELECT user_id, "event_ts", event_type
FROM company_data."12345"
ORDER BY "event_ts" DESC
LIMIT 25

Plain-English: "I'm pulling the 25 newest records from the events dataset, showing the user, the timestamp, and the event type."

"Group by something"

SELECT event_type, COUNT(1) AS event_count
FROM company_data."12345"
WHERE "event_ts" >= CAST('2026-04-19' AS timestamp)
GROUP BY event_type
ORDER BY event_count DESC

Plain-English: "I'm counting events since April 19, 2026, broken down by the event type, with the most common types listed first."

"Cross-dataset join"

SELECT u.user_id, u.email, COUNT(e.event_id) AS event_count
FROM company_data."12345" u
LEFT JOIN company_data."67890" e ON e.user_id = u.user_id
GROUP BY u.user_id, u.email
ORDER BY event_count DESC
LIMIT 50

Plain-English: "I'm combining the users dataset with the events dataset on the shared user id, counting how many events each user has, and showing the 50 most active users first."

Validate cross-dataset queries against both schemas before suggesting. Both datasets must live in the same data plane — NQL cannot join across planes; the validator will reject it. Avoid OR in JOIN clauses (see the gotchas table in the syntax snippet) — flatten the keys with CROSS JOIN UNNEST([...]) or UNION two single-key joins.

References

  • references/EDGE_CASES.md — nonexistent columns, wildcard scans on huge datasets, --run cost warnings, validator-vs-user disagreement, schema drift. Read when something doesn't add up.
  • references/HARNESS_FALLBACK.mdnarrative-mcp unavailable (paste-driven schema, no server validation), AskUserQuestion fallback. Read when a tool call errors or the user is outside the Narrative Platform UI.
  • references/PERCENTILE_DISTRIBUTION.md — percentile/distribution patterns on the Snowflake data plane where APPROX_PERCENTILE and PERCENTILE_CONT aren't usable. Read for distribution shape, quartiles, thresholds, skew.
  • references/NQL_GOTCHAS.md — full failure-mode catalog (GEOMETRY, OR-in-JOIN, cross-plane, QUALIFY-in-CMV, percentile fallbacks, reserved keywords, dataset-id quoting). Read when a draft fails validation or a passed-validation query 500s at run.
  • references/NQL_VALIDATION_ERRORS.md — error-message → canonical-fix cheat sheet. Read when narrative_nql_validate returns an error and you want the shortest path to green.
  • references/NQL_QUOTING_AND_TABLE_REFS.md — schema list (company_data / <provider_slug> / narrative), unique_name-vs-numeric-id rules, reserved-words catalog, Rosetta Stone scope syntax. Read when a happy-path company_data.<dataset_name> reference isn't enough.
  • references/NQL_ASYNC_DEEP.mdcompleted payload shape per job type, the materialize-view → sample → describe dance, sibling async tools. Read when a job finishes but you can't find the rows.
  • narrative-knowledge-base MCP — /concepts/nql/…, /cookbooks/nql/…, /api-reference/nql/…, /reference/integrations/mcp-server for parameter contracts (data_plane_id, compute_pool_id). For gotchas: /guides/nql/troubleshooting, /nql/general/explicit-columns, /nql/general/reserved-keywords, /nql/commands/create-materialized-view, /concepts/primitives/data-planes, /guides/nql/query-optimization/avoid-or-in-join, /cookbooks/nql/performance-patterns.
  • plugins/narrative-common/skills/generate-rosetta-stone-mappings/references/EXPRESSION_SYNTAX.md — sibling reference covering timestamp parsing, enum handling, reserved-name nesting.

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-common:write-nql (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.

Frequently asked questions

What to verify before installation and use

What does the write-nql source document cover?

Write, validate, and (optionally) execute an NQL query against a Narrative dataset. Drafts the query from the user's question, runs `narrative_nql_validate` until it compiles, explains the query in plain English, and only runs it on explicit approval (or when invoked with `--run`).

How do I install write-nql?

The source record exposes this install command: npx skills add https://github.com/narrative-io/narrative-skills-marketplace --skill "plugins/narrative-common/skills/write-nql". Inspect the command and pinned source before running it.

Which Agent platforms does the source record declare?

The pinned source record declares support for: claude code.

Alternatives

Compare before choosing

Computed 1002,670

aaron-he-zhu/aaron-marketing-skills

social-selling-planner

Use when the user asks to "set up my founder social-selling routine", "build a daily engagement block for target accounts", or "turn funding / hiring signals into selling plays"; produces the founder/seller daily operating block — a time-boxed engagement-block spec (substantive value-add comments on target-account posts, never a pitch), warm-touch-before-ask cadence rules, trigger-response plays consuming the social-pulse-monitor B2B trigger watchlist (funding / hiring / launch signals), and a q

Computed 100147

oaustegard/claude-skills

featuring

Generate hierarchical _FEATURES.md files that describe what a codebase DOES from a user/consumer perspective, anchored to source symbols via tree-sitting. Supports large complex codebases through feature-driven decomposition into sub-feature files. Uses a multi-pass synthesis: orientation → detail → overview rewrite. Use when someone says "what does this do", "document features", "feature inventory", "_FEATURES.md", or needs to understand a codebase's purpose before modifying it. Complements tre

Computed 100108

apollographql/skills

skill-creator

Guide for creating effective skills for Apollo GraphQL and GraphQL development. Use this skill when: (1) users want to create a new skill, (2) users want to update an existing skill, (3) users ask about skill structure or best practices, (4) users need help writing SKILL.md files.

Computed 10061

terrylica/cc-skills

draft-park

Park a draft message/text in macOS Notes for the operator to review and edit, then read it back before acting (e.g. before sending to a real person). Notes is the source of truth (AppleScript CRUD, iCloud-synced, provenance-stamped with the Claude Code session UUID); Stickies is a best-effort view-only desktop mirror. Use whenever you draft something a human should confirm/edit before it is sent or committed — messages, replies, announcements, anything outbound. TRIGGERS - park this draft, park