malloydata/publisher/malloy-model
Claude Code and agent skill by malloydata/publisher
- community
Checking that the SKILL.md file is still in the repository.
Build Malloy semantic models with base source and joined source files. Use when creating or modifying .malloy files, user asks to "create a malloy model", "add dimensions", "add measures", "create a source", or any Malloy model authoring task.
Install
npx vskill@latest install malloydata/publisher/malloy-modelSKILL.md
Building Malloy Models
Tool names are written bare here -get_context,execute_query,search_malloy_docs. The exact prefixed name depends on the host surface; match each against the tools you actually have.
Getting Started (New Projects)
If no .malloy files exist yet, do discovery and propose a structure first, then return here to build base source and joined source files. Keep proposals and the analysis behind them in the conversation.
File structure convention (a flat layout at the package root is the simplest default):
Versions for new packages should start at "0.0.1".
Before creating any files, check for an existing publisher.json in the target directory. If one exists for a different package, create a new subdirectory for your package, don't overwrite another package's config.
In Publisher an environment is a project, and Publisher is single-tenant, so there is no org/tenant layer to model around: one environment holds one set of packages.
Prior Art Dispatch
If your discovery turned up existing modeling patterns to mirror (a derived table, UNNEST joins, a review or curation pass), read the relevant reference before building.
| Pattern found in prior art | Reference to read | |
|---|---|---|
| Derived table (PDT/NDT) | skill:malloy-lookml-review build-derived-tables guidance | |
| UNNEST joins or struct access | skill:malloy-lookml-review build-unnest guidance | |
| Review pass for coverage | skill:malloy-lookml-review review-coverage guidance | |
| Curate pass with visibility seeds | skill:malloy-lookml-review curate-visibility guidance |
Base Source Templates
Base Source (Simple Mode)
Every `dimension:` needs `name is expr`. A bare column name likedimension: speciesis a parse error (unexpected '}', ormissing 'is' before 'measure:'when another declaration follows). Raw columns are already usable ingroup_by/selectwithout any declaration, so only add adimension:when deriving or renaming a field (e.g.revenue is price * quantity).
Base Source (Curated Mode with Access Modifiers)
Computed Source (from Query)
Wrap the query in parentheses and extend it. from(...) was removed from the language and no longer parses (unexpected 'from').
For advanced query-based source patterns (window functions, pipelines), see reference/query-sources.md.
Joined Source File Template
Base vs Joined Sources
| Base Joined Source File | Joined Source File | ||
|---|---|---|---|
| Contains | One table's fields | Joins between base sources | |
| Dimensions | Intrinsic to this table only | Cross-source (require joins) | |
| Measures | Single-table aggregations | Cross-source aggregations | |
| Joins | None (or only lookup joins intrinsic to the source) | Defines relationships between base sources | |
| Views | None, in schema-first (see below) | None, in schema-first (see below) | |
| One per | Physical table or computed source | Analytical domain |
The "no views" rule is schema-first only. A schema-first model is built before anyone has asked a question, so any view in it is a guess. It does not apply to the analysis-first workflow (skill:malloy-model-as-you-go), where every view is a question that was asked and verified - there, saving a view or a dashboard is the right call, in the model file next to the measures it uses. Analysis-first still models everything else properly: documented dimensions, measures, and joins.
Two more rules here are schema-first only. Analysis-first should skip access modifiers and curation - there is no discovery surface to curate when every field was paid for by a question - and skip one-file-per-table, keeping a single domain file until it genuinely gets unwieldy.
Key Rules
- Every `dimension:` needs `name is expr`: a bare
dimension: speciesis a parse error. Raw columns are queryable directly ingroup_by/select; only declare adimension:to derive or rename a field. - Define joined tables before referencing them, use
importstatements in multi-file architecture - Use `nullif(denominator, 0)` for all division
- Alias joined fields before using in `order_by`:
group_by: yr is table.year - Verify join paths exist before referencing
a.b.field(each hop needs explicit join) - Pick syntax: value BEFORE condition,
pick 'Small' when size < 10 - `where:` vs `having:`: Use
where:for row filters,having:for aggregate filters - `rename:` composes with `include {}`, but only in one order: the
extend { rename: }must come before theinclude {}, which then names the field by its new name. Reversed, it fails withCan't find field 'X' to set access modifier. For a cleaner column name without a rename,internal:+dimension:is still the lighter move (mark `Typeasinternal, adddimension: order_type isType). Seeskill:malloy-gotchas-modeling` § Field Management - Mark raw columns `internal` when a derived dimension replaces them
- Check for duplicate rows before building measures
- When both a combined table (all types) and filtered/split tables exist, prefer the split tables
- DRY: define measures/dimensions in base source files, not inline in views
- Lay out a new file the same way throughout: two-space indentation, no tabs, one blank line between top-level declarations (consecutive
importlines stay together), no trailing whitespace, and a long line broken after a comma or before an operator, at whatever width the project keeps to. In a project whose files already use another layout (tabs, four spaces), a new file matches them. - An edit keeps the file's own layout: change only the lines the request needs, and never reindent or rewrap a line you weren't asked to change, so the diff shows the change and nothing else.
- Never write a threshold, tier boundary, or bucket cutoff you chose yourself. Every boundary in a
pickexpression or filtered measure is user-supplied, distribution-derived (querymin/p25/p50/p75/p95first and show the evidence; Malloy has nopercentilefunction, so use the two-stage query inskill:malloy-discover§ Example Queries; seeskill:malloy-define§ Data-driven proposals), or explicitly flagged as an assumption in its#(doc). A hardcoded cutoff nobody confirmed is a business decision shipped as fact.
Parameterizing sources with given: (preferred)
Native Malloy `given:` parameters are runtime values a model reads in its own logic; callers supply them at query time. #(filter) is deprecated: never add one. Enable givens with ##! experimental.givens at the top of the model.
A given belongs in the model only when the data's meaning or access depends on it:
- a row-level gate:
#(authorize),#(access_filter), a#(secure)given,$GROUPS; - a value every query must supply (the no-default row in the table below);
- a parameter a measure, dimension or view is defined with;
- scoping the author means to apply to every consumer of the source: chat, MCP and every report.
A filter a reader picks on a dashboard or notebook is not modeling. Manufacturer, a date range, region, a "major recalls only" toggle: don't add it to a source. Leave the source unfiltered, and let the report declare the given and bind it on its tiles (skill:malloy-reports). A model-level where: applies to every read of the source, chat and every other report included, and a report cannot take it over, because binding is per declaration, not per name. The report builder shows such a filter but cannot remove or reconfigure it.
A given is declared bare but referenced with a `$` sigil in expressions ($EVENT_DATE), as above.
- An optional given in the model gets a neutral, match-all default (a
filter<>given defaulting tof''), so an unsupplied value returns unfiltered rows. Because it bakes an always-onwhere:into the source, a non-neutral default (a date floor, say) applies to _every_ read of the source. Defaults must be Malloy literals. - Givens don't auto-inject a `where:`. Unlike
#(filter), you write the filter expression that references the given yourself (e.g.where: dimension ~ $given_name). - Every `#(filter)` use has a `given:` form. Never add a
#(filter)annotation to a model, not even forrequired,implicit, or a date/number range:
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/malloy-model/SKILL.md
Popularity
- Repository stars
- 97
- Repository forks
- 24
- 7-day trend score
- 23
Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 7 Aug 2026, 3 months ago.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/malloydata/publisher/malloy-model)