Agent skill

Malloy Materialization

by malloydata in malloydata/publisher

Add and debug Malloy Persistence materializations in a package - persist an expensive source so queries read a pre-built table.

MITAuto-check passedDatabases

Install Malloy Materialization

skills CLI
$ npx skills add malloydata/publisher --skill malloy-materialization -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install malloydata/publisher malloy-materialization --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/malloydata/publisher.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/malloy-materialization .claude/skills/malloy-materialization && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
malloy-materialization
GitHub stars
116
Token cost
~4.2k tokens
SKILL.md length
2,316 words
Files
2
Skills in repo
29
Repo updated
First seen
Licence
MIT

At a glance

Add and debug Malloy Persistence materializations in a package - persist an expensive source so queries read a pre-built table.

  • Works in 4 steps: ##! experimental.persistence on EVERY… → #@ persist name="..." on a query-based… → Package persistence policy in… → …
  • Databases work in your project
  • SKILL.md covers The recipe (get this right and…, Building and refreshing…, Serve-time routing is… and Confirming it worked, plus 3 more sections
  • Instructions only: no scripts, shell commands, URLs or credentials in SKILL.md

What it does

Malloy Materialization is an agent skill from malloydata/publisher. Add and debug Malloy Persistence materializations in a package - persist an expensive source so queries read a pre-built table. Read this whenever the user wants to materialize a source, add a persist annotation, speed up a slow source, tune what to persist, or asks why a persist source isn't building.

Its SKILL.md is about 4.2k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files (for example `reference/tuning.md`).

It sits in Databases. It works with SQL. The repository describes itself as: Publisher is the open-source analytics engine for Malloy. It lets you define data models once — and use them everywhere. The licence is MIT.

When your agent uses it

  • Databases work in your project

Example prompts

  • “/malloy-materialization”

Workflow steps

4 steps, taken from the first numbered list in SKILL.md.

  1. ##! experimental.persistence on EVERY .malloy file in the package - not only the file that declares the persist source. Either form…
  2. #@ persist name="..." on a query-based source, with the name quoted
  3. Package persistence policy in publisher.json (all optional)
  4. Reads vs writes. The persist source can read any dataset the connection can read; the persist target (name='s dataset) must be a dataset…

What it can do on your machine

Read from SKILL.md and the folder at commit acc1acd. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md (its code samples are malloy and jsonc).

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Malloy Materialization loads about 4.2k tokens when it runs. Until then it costs about 82 tokens; SKILL.md has 2,316 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~82
When it runs · the whole SKILL.md, loaded when a task matches
~4.2k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from malloydata/publisher at commit acc1acd, republished under its MIT licence (© malloydata). 2,316 words, ~4,217 tokens.

Download SKILL.mdSave it as .claude/skills/malloy-materialization/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.
name
malloy-materialization
description
Add and debug Malloy Persistence materializations in a package - persist an expensive source so queries read a pre-built table. Read this whenever the user wants to materialize a source, add a persist annotation, speed up a slow source, tune what to persist, or asks why a persist source isn't building.
<!--
Copyright (c) Credible Data Inc.
SPDX-License-Identifier: MIT
-->

Materialization (Malloy Persistence)

Materialize an expensive source once so queries read a pre-built warehouse table instead of recomputing it every time. You tag a source #@ persist, a materialization run builds it into a physical table, and queries against it are rewritten to read that table.

The #1 gotcha, up front: if a persist source isn't materializing, it is almost always one of two things - a .malloy file in the package missing the ##! experimental.persistence flag (which aborts the whole package's build plan), or no build ever ran (a standalone Publisher does not build on publish - see Building and refreshing). Jump to Debugging a no-op build.

Deciding what to persist, what to stop persisting, and how to schedule it (making a package cheaper or faster): read reference/tuning.md. It reads the materialization history with the malloy-pub CLI and proposes changes; it recommends and does not edit.

The recipe (get this right and it just works)

  1. ##! experimental.persistence on EVERY .malloy file in the package - not only the file that declares the persist source. Either form enables it:

    • ##! experimental.persistence, or
    • ##! experimental { access_modifiers, sql_functions, persistence } (add persistence to the existing list).

    Why every file: the build plan is computed by asking every .malloy file in the package for its persist sources, and that call throws on any file whose model lacks the flag (Model must have ##! experimental.persistence). One unflagged helper or import file, even one with no persist source of its own, aborts the whole package's build plan, so every persist source in the package drops out. This is the most common cause of a no-op build.

  2. #@ persist name="..." on a query-based source, with the name quoted:

    malloy
    #@ persist name="my_dataset.my_table"
    source: my_rollup is some_source -> { group_by: ...; aggregate: ... }
    • Only query_source and sql_select sources are persistable - a source whose definition has a -> { ... } pipeline or a conn.sql("..."). This includes one refined by a trailing extend { ... }. What is not persistable is a plain extend over a bare conn.table(...); a #@ persist on such a source is silently ignored (its annotation is never read) - that one source just won't materialize, and the rest of the package still builds.
    • Quote the name. name="my_table" (or a path name="dataset.table" / name="project.dataset.table") is required. A bare name=my_table always fails the build/publish with persist annotation name must be quoted (a raw-source scan that hard-stops); it never silently no-ops.
    • name= is the target table name. In a standalone Publisher this is the physical table (rebuilt in place); a hosted (control-plane) deployment builds it under a content-addressed generation name. In both, the source's identity for reuse is a content address of its connection and canonical SQL (its sourceEntityId), so republishing unchanged persist logic reuses the existing table and changing the logic builds fresh.
  3. Package persistence policy in publisher.json (all optional):

    jsonc
    {
      "name": "my-package",
      "materialization": {
        "scope": "package",  // default; "version" = each published version owns its own tables
        "freshness": { "window": "24h", "fallback": "live" },
        "queryMetadata": { "team": "finance" }  // tags the build's backend statements
      }
    }

    Enforced at publish (strict), on edits (strict), at load (warn, still serves), and by the scheduler (an offending package is skipped):

    • scope: package (default; artifacts reused across published versions) or version (each artifact owned by one version). Package-level only; there is no per-source scope. A root-level scope is the deprecated home and still works, with a warning; declaring both homes with different values is rejected.
    • materialization.freshness (window + fallback of live/stale_ok/fail) is the objective a hosted control plane enforces by refreshing the table to meet it (fallback: "live" serves live compute while stale/absent). A standalone Publisher does not act on freshness for refresh - see Building and refreshing.
    • materialization.queryMetadata is a bag of string properties attached to every statement the build issues, for the backend's own cost attribution (Snowflake QUERY_TAG, BigQuery job labels, a leading SQL comment elsewhere). Overridable per source with #@ persist queryMetadata.<name>="<value>". Observability only: it never changes what gets built. See docs/query-metadata.md.
    • materialization.schedule is a 5-field UTC cron (min hour dom mon dow; L/W/#/? rejected). It requires scope: "version" and is mutually exclusive with freshness. This is how a standalone Publisher refreshes on a cadence.
  4. Reads vs writes. The persist source can read any dataset the connection can read; the persist target (name='s dataset) must be a dataset the connection can write (typically a scratch dataset).

Building and refreshing (standalone vs. hosted)

A #@ persist tag declares what to materialize; it does not by itself build anything.

  • Standalone Publisher: publishing or loading a package only computes its build plan - no table is built until a materialization run executes. Trigger one explicitly (malloy-pub materialize --package <pkg> --wait, or the materialization API), or turn on the opt-in local scheduler (off unless PUBLISHER_LOCAL_MATERIALIZATION_SCHEDULER is set) to fire the package's schedule cron. Refresh is a re-run or that cron; freshness is not a refresh trigger here, so a freshness-only standalone package builds once and is not auto-refreshed.
  • Hosted (control-plane) deployment: the build runs automatically on publish, best-effort - a build failure does not fail the publish (which is why a broken persist can look like a silent no-op), and the control plane drives refresh to meet the freshness objective.

Either way, a successful publish alone does not prove a table exists - confirm the build separately.

Serve-time routing is query_source-only (today)

Both persistable types build a table, but only a query_source (a -> { ... } pipeline) is rewritten to read it at query time. A raw sql_select (conn.sql("..."), including conn.sql("...") extend { ... }) builds its table and then the query path re-inlines its SQL, so the table is built and never read, and queries are no faster. If you have raw SQL you want served from a table, wrap it in a thin query_source and persist that:

malloy
source: x_raw is my_conn.sql("select ...")
#@ persist name="scratch_dataset.x"
source: x is x_raw -> { select: * }

Confirming it worked

After a build runs, re-run one of the source's queries - a persisted query_source should return quickly, reading the pre-built table instead of recomputing the upstream. Your host also reports each persisted source as ready with its physical table name (a materialization run detail, CLI listing, or materialization view, depending on the host); if nothing is listed, either no build ran (standalone) or the build plan was empty - see Debugging a no-op build.

Debugging a no-op build

Symptom: no table was built and the source still recomputes on every query. Check, in order:

  1. Did a build actually run? On a standalone Publisher, publish/load does not build - run malloy-pub materialize (or enable the scheduler). "Publishes fine, no table" is the expected standalone state, not a model bug. On a hosted deployment the build is automatic but best-effort, so a failure is silent - look for a FAILED run.
  2. A .malloy file missing the persistence flag (the most common real bug). Every model file's ##! line needs persistence, including pure helper/import files with no persist source - one unflagged file aborts the whole package's build plan.
  3. An unquoted persist name - a bare name=foo always hard-stops the build/publish with persist annotation name must be quoted; use name="foo". (If you got no error at all, it isn't this.)
  4. A #@ persist on a non-persistable source - a bare extend over conn.table(...) is silently ignored, so that source won't materialize (the rest of the package is unaffected). Tag a query_source / sql_select instead.
  5. A persisted raw sql_select that builds but is never read - if the table exists yet queries are no faster, it's the serve-routing gap above; wrap the sql_select in a query_source.

Isolation test - add a trivial, self-contained persist source in its own file and rebuild:

malloy
##! experimental.persistence
source: smoke_raw is my_conn.table('some_dataset.some_table')
#@ persist name="scratch_dataset.persist_smoke_test"
source: persist_smoke is smoke_raw -> { aggregate: n is count() }
  • If even this doesn't build (after a real materialization run), the whole package's plan is aborting - a sibling .malloy file is missing the flag. Fix rule 1 across the package.
  • If the smoke source does build but your real one doesn't, your real source is the problem - a non-persistable type (a bare extend), or its own file's flag.

Delete the smoke file and drop its table afterward.

Show full SKILL.md (1,067 more words)Show less

Persisting a #(access_filter)-gated source

A gated source can be persisted, but only on one tier and only in one shape, and the thing to be careful about is not refused by anything - you have to decide it.

  • storage= and #@ preaggregate always refuse a gated source, naming it. The build skips a refused source, records it on the run (metadata.refusedSources), and builds the rest of the package. A run fails on a refusal only when every authored source it targeted was refused, or when sourceNames named this one; a refused rollup never fails it. (#@ persist storage=<name> is the tier that materializes into a separate registered storage destination and serves from there, rather than building in the source's own connection; #@ preaggregate stores a rollup Publisher derives from a measure you annotated with a grain, rather than a source you wrote.) A rollup also groups across the gated column, so it could not be row-filtered afterwards even in principle.
  • A colocated #@ persist (no storage=) is admitted when the gate is provably the entry point's own row filter. It is refused when the gate is reached only through a join, inherited from a base the compiler cannot attribute cleanly, or does not classify as a row filter at all. The gate is found through the import -> rename -> query_source chain, so a gate the persisted source did not declare itself still counts.

What to be wary of. Persisting does not weaken the gate: it changes only where rows are read FROM, and the gate still runs live on every query as that query's own WHERE, so filtered rows come back filtered. What freezes is the column the gate filters on. A row whose access decision changes - it changes owner, say - keeps being served under its OLD decision until the next rebuild. That is a stale access decision, not merely stale data, and nothing raises an error.

None of this is needed for the gate to work. It is enforced live on every query either way; what needs a bound is how long a stale decision can survive. Of the three controls that look like that bound, only the first is:

  • materialization.freshness { "window": "24h", "fallback": "live" } is the bound. The serve path re-checks freshness per query, so once the artifact ages past the window it drops out of the serving set and the query recomputes live, correctly filtered - whether or not a rebuild ever lands. Three details decide whether you actually get that. fallback must be live: under stale_ok a stale artifact keeps being served, which voids the bound, and window and fallback resolve independently per layer, so a package-level stale_ok silently defeats a window you set on the source. That is a statement about layers, which do not combine - not about siblings, below. Prefer the per-source spelling #@ persist name="..." freshness.window="24h" freshness.fallback="live" over the package-wide materialization.freshness key: the gated source is what needs the bound, and setting it package-wide forces every other persisted source to recompute once stale too. And a content-identical sibling shares the artifact, so it shares the window: reuse is keyed on the content-addressed sourceEntityId, which folds the connection and the SQL but not the source name, so two persist sources whose bodies compute the same SQL resolve to one table carrying one freshness policy. The tightest window any of them declares governs all of them - a sibling declaring nothing cannot loosen yours, and yours pulls that sibling's reads off the table once it lapses. A sibling's stale_ok cannot void your bound either: the fold keeps whichever fallback bounds staleness, so the layer rule above does not carry over here. If two sources need genuinely different windows, give them genuinely different SQL.

    Both of those are properties of the host that assembles the manifest, not of the annotation. Where the host does not fold, which sibling's policy reaches the wire is unspecified; and a host that folds at manifest-assembly time typically applies it when a version's manifest is next published rather than retroactively to manifests already distributed - so you can declare the window correctly and not have it in force yet.

  • A cron alone is not a bound. A failed build or a stopped scheduler leaves the source serving its old decisions indefinitely. freshness and schedule are mutually exclusive; for a gated source, take the window.

  • refresh="incremental" does not bound revocation. The delta only re-reads rows in [covered_through, frontier), so a row that changes owner without its watermark advancing is never re-read - while the entry still reports an advancing coveredThrough and reads as healthy. Only a full rebuild recomputes the gating column.

And the window only binds where the serving manifest carries it. Freshness is enforced from fields a control plane stamps onto the manifest it distributes; a Publisher that serves what it just built binds the table with no dataAsOf and no window, and an entry carrying no window never ages out. So on a standalone deployment the declared window is inert and the artifact serves until the next full rebuild - which leaves a rebuild cadence you actually verify as the only bound, and makes leaving a revocation-sensitive source unpersisted the safer call.

When recommending #@ persist on a gated source, pair it with a freshness window and say out loud what staleness the author is accepting. A gated source with neither a window nor a full-rebuild cadence has no bound on how long a revoked row keeps being served.

Gotchas

  • Every .malloy file needs the persistence flag - one unflagged file aborts the whole package's build plan. (A #@ persist on a non-persistable source, by contrast, is silently ignored and does not affect other sources.)
  • A tag doesn't build - a standalone Publisher materializes only on an explicit run or its scheduler; only a hosted control plane builds on publish.
  • Serve-time routing is query_source-only - a raw sql_select builds a table the query path doesn't read; wrap it in a query_source.
  • Quote the name - a bare name= always hard-stops the build.
  • Republishing unchanged persist logic reuses the table - reuse is keyed on the content-addressed sourceEntityId, not the name=.
  • Removing a persist source (or a smoke test) does not drop its table - physical-table cleanup is the caller's responsibility; drop it yourself.
  • A #(access_filter)-gated source freezes its gating column when persisted - the gate still runs live, but a revoked row keeps being served under its old access decision until the next rebuild. storage= and #@ preaggregate refuse a gated source outright. See Persisting a #(access_filter)-gated source.

© malloydata, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 1 other file in skills/malloy-materialization of malloydata/publisher.

  • SKILL.md
  • reference/tuning.md

Open the folder on GitHubat commit acc1acd

Compare with similar skills

Malloy Materialization next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Malloy Materialization compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Malloy Materialization this skillmalloydata/publisher116—~4.2kAutomated safety check: PassMIT
Relational Query ProcessorFoundationDB/fdb-record-layer675—~1kAutomated safety check: PassApache-2.0
Diesel Guardayarotsky/diesel-guard121—~3.1kAutomated safety check: PassMIT
Releaserogerpadilla/uql125—~842Automated safety check: PassMIT
Srtd Devt1mmen/srtd105—~2.3kAutomated safety check: PassMIT
Logfire Querypydantic/skills140—~2.2kAutomated safety check: PassMIT

Similar skills

  • Relational Query Processor

    FoundationDB/fdb-record-layer

    Specialized skill for working in the fdb-relational-core SQL processing layer — parser, plan generator, and Cascades planner.

    675 GitHub stars~1k tokensUpdated today
    DatabasesAuto-check passed
  • Diesel Guard

    ayarotsky/diesel-guard

    Lints Diesel and SQLx Postgres migrations for unsafe schema changes that lock tables or cause downtime, and authors custom Rhai checks.

    121 GitHub stars~3.1k tokensUpdated 9 days ago
    DatabasesAuto-check passed
  • Release

    rogerpadilla/uql

    Cut and publish a uql release - review the change, changelog entry, commit, version bump and tag, GitHub Release, npm publish, docs site.

    125 GitHub stars~842 tokensUpdated today
    DatabasesAuto-check passed
  • Srtd Dev

    t1mmen/srtd

    Expert knowledge for developing the SRTD codebase itself. An agent skill from t1mmen/srtd.

    105 GitHub stars~2.3k tokensUpdated 1 mo ago
    DatabasesAuto-check passed
  • Logfire Query

    pydantic/skills

    Official

    Query and analyze Logfire telemetry data — traces, logs, spans, metrics, summaries, and SQL results.

    140 GitHub stars~2.2k tokensUpdated 6 days ago
    DatabasesAuto-check passed
  • Sap Abap

    secondsky/sap-skills

    Comprehensive ABAP development skill for SAP systems. An agent skill from secondsky/sap-skills.

    460 GitHub stars~3.9k tokensUpdated 2 days ago
    DatabasesAuto-check passed

More from malloydata/publisher

All 29 skills in this repo
  • Eval Answer

    malloydata/publisher

    Score one analytical answer against a verified golden, and score which of the entities the golden depends on retrieval delivered to the answerer.

    116 GitHub stars~4.3k tokensUpdated today
    Auto-check passed
  • Fix Scan Finding

    malloydata/publisher

    Fix a CRITICAL Trivy finding that is failing CI in this repo (a vulnerability, misconfiguration, or secret from security-scan.yml or image-scan.yml), or add, review, or retire an entry in…

    116 GitHub stars~5.1k tokensUpdated today
    Auto-check passed
  • Eval Import

    malloydata/publisher

    Turn a list of questions into an eval set, whatever shape it arrived in: a JSONL a customer sent, a CSV, a spreadsheet export, a markdown doc, an email thread, or a pull from production logs.

    116 GitHub stars~5.9k tokensUpdated today
    Auto-check passed
  • Eval Loop

    malloydata/publisher

    Conduct a local Publisher evaluation loop in five steps: scrape/run, eval, diagnose, improve, checkpoint.

    116 GitHub stars~7.8k tokensUpdated today
    Auto-check passed
  • Eval Improve

    malloydata/publisher

    Make the smallest safe Malloy model edit that closes a diagnosed model-owned gap, with a probe receipt for every factual claim.

    116 GitHub stars~2.8k tokensUpdated today
    Auto-check passed
  • Eval Judge

    malloydata/publisher

    Decide whether ONE answer matches its golden, and say whether you believe the golden.

    116 GitHub stars~3.4k tokensUpdated today
    Auto-check passed

Works with

Questions about Malloy Materialization

What does Malloy Materialization do?

Add and debug Malloy Persistence materializations in a package - persist an expensive source so queries read a pre-built table. Malloy Materialization is an agent skill from malloydata/publisher. Add and debug Malloy Persistence materializations in a package - persist an expensive source so queries read a pre-built table.

When should I use Malloy Materialization?

Malloy Materialization fits situations like: databases work in your project.

How do I install Malloy Materialization in Claude Code?

Run `npx skills add malloydata/publisher --skill malloy-materialization -a claude-code`. Or copy the skill folder (skills/malloy-materialization in malloydata/publisher) into .claude/skills/malloy-materialization in your project. Claude Code loads it when a task matches its description.

How do I install Malloy Materialization in Codex?

Run `npx skills add malloydata/publisher --skill malloy-materialization -a codex`. Or copy the skill folder (skills/malloy-materialization in malloydata/publisher) into .agents/skills/malloy-materialization in your project. Codex loads it when a task matches its description.

Can I use Malloy Materialization in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add malloydata/publisher --skill malloy-materialization -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/malloy-materialization, .gemini/skills/malloy-materialization, .github/skills/malloy-materialization and .opencode/skills/malloy-materialization in your project.

What does Malloy Materialization need to run?

SKILL.md names no scripts, command-line tools or credentials: Malloy Materialization is instructions for the agent only.

Does Malloy Materialization access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Malloy Materialization safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Malloy Materialization use?

Malloy Materialization is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Malloy Materialization use?

About 4.2k tokens (SKILL.md is roughly 17k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Malloy Materialization?

Skills that share tags, products or a category with Malloy Materialization: Relational Query Processor (FoundationDB/fdb-record-layer, 675 stars), Diesel Guard (ayarotsky/diesel-guard, 121 stars), Release (rogerpadilla/uql, 125 stars) and Srtd Dev (t1mmen/srtd, 105 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Malloy Materialization?

malloydata (a GitHub organization) maintains it in malloydata/publisher, which has 116 GitHub stars. The repository holds 29 skills in this directory. The repository was last updated on October 7, 2026.

Source: malloydata/publisher on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.