Skip to content
Agentmart
Submit a tool

malloy-reports

Build or modify a Malloy Publisher report: a .malloy file with an ## artifact tag, written at the package root unless asked otherwise, laid out as a grid (a…

Skill by malloydata

IndexedMIT★ 118
Write a review

Chalk Labs

Need help deploying malloy-reports?

Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.

Book a call

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/malloydata-malloy-reports

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Build or modify a Malloy Publisher report: a .malloy file with an ## artifact tag, written at the package root unless asked otherwise, laid out as a grid (a dashboard, scanned at a glance) or linear (a notebook or data story, read top to bottom), with filter controls rendered from givens and # drill click-through. Use when the user asks for a dashboard, a notebook, a report, a data story, a filterable view, or drill-through between views, and no code is wanted. Legacy .malloynb files are read, not authored.

Use cases: documents, data

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/malloy-reports

What's inside

Skill name
malloy-reports
Description
Build or modify a Malloy Publisher report: a .malloy file with an ## artifact tag, written at the package root unless asked otherwise, laid out as a grid (a dashboard, scanned at a glance) or linear (a notebook or data story, read top to bottom), with filter controls rendered from givens and # drill click-through. Use when the user asks for a dashboard, a notebook, a report, a data story, a filterable view, or drill-through between views, and no code is wanted. Legacy .malloynb files are read, not authored.
License
MIT
Instructions
299 lines in SKILL.md · view on GitHub

Files (3)

  • SKILL.md
  • grid.md · reference/
  • linear.md · reference/

SKILL.md (first lines)

<!--
Copyright (c) Credible Data Inc.
SPDX-License-Identifier: MIT
-->

# Publisher Reports

> A `.malloy` file with an `## artifact { layout=… tiles=[…] }` tag **is** a report. It imports the model, names the views to show, binds the filtering on those views, and says how to lay them out. Publisher discovers it at package load, renders the filter controls from the givens its tiles reference, offers `# drill` click-through, and serves it at `/<env>/<pkg>/<path>`, its package path without `.malloy`. No code, no build step.

What other tools call a dashboard and a notebook are both reports here. The **layout** is the only difference:

- **`layout=grid`**: tiles across a grid behind one filter row, scanned at a glance (a dashboard). Read `reference/grid.md` before you lay one out.
- **`layout=linear`**: tiles and prose top to bottom, read in order (a notebook, a data story). Read `reference/linear.md` before you write one.

Everything else (filters, text tiles, drill, the builder, the lint, the write path) is the same for both, and switching layout later is a one-property edit.

## Where the file goes

- **Write a new report at the package root**, as `<slug>.malloy`, unless the author asks for another location. No directory is required, and none is a convention: a report can live anywhere in the package except `public/`, and the root `index.malloy` is never a report. Do not create a `dashboards/` or `notebooks/` directory for it.
- **Always write `layout=grid` or `layout=linear`** in the tag. A tag without one is read by a fallback rule, and a linear report that lists tiles without `layout=linear` is read as a grid. `kind=` is no longer read: a tag that sets it makes no report (`report-kind-removed`), so replace it with `layout=`.
- **The package path is the report's identity.** `sales/q3.malloy` is the report `sales/q3`: its URL, its `GET …/reports/{path}` address, and its `# drill` target. Two reports may share a basename in different directories.
- **Imports are relative to the report's own location.** At the root: `import { order_items } from "storefront.malloy"`. In a subdirectory: `"../storefront.malloy"`.
- **An untagged `.malloy` file is an ordinary model**, wherever it lives. Shared definitions several reports import go in one.

## When this is the right tool

| The user wants                                       | Use                                                          |
| ---------------------------------------------------- | ------------------------------------------------------------ |
| A recurring, at-a-glance view behind shared filters  | this skill, `layout=grid`                                    |
| A narrative, with prose between the numbers          | this skill, `layout=linear`                                  |
| A quick write-up of queries you already ran          | `skill:malloy-analysis-report`, which writes a linear report |
| Custom design, branding, or interactions beyond tags | an HTML data app                                             |
| The model itself: sources, measures, joins           | `malloy-model`                                               |

Interactivity is not the axis: both layouts get filter controls, URL-addressable state, Apply batching, and `# drill`. Pick on the shape of the document: scanned at a glance is grid, read top to bottom is linear. When in doubt, linear.

## Build sequence

Tool names below are bare (`get_context`); match each against the tools you actually have, since a host may prefix or rename them. A host names its compile and reload tools its own way. Where you have none, use the REST endpoint named beside the step.

1. **READ THE MODEL FIRST.** Get the real source, view, dimension, and given names from the package: `get_context` if you have it, otherwise the REST model endpoint for `index.malloy` (it answers 404 for a file off the surface) or the `.malloy` files. Never guess a name. A guessed field in a query fails the whole package load, not just that one report; a guessed tile or suggest source is quieter, and only shows up in the package warnings. If the package root holds an `index.malloy` (every scaffolded package does), a tile reads only the sources that file exports.
2. **PICK THE LAYOUT AND THE TILES.** Which views, in what order, and on a grid how wide each sits. A query tile is a `view:` you declare in the report, named as `"source -> view"`.
3. **DECLARE THE GIVENS** the report will filter by, in the report file itself, with their control tags (see "Filter controls"). A report holds its own filters: the builder edits the report file and nothing else, so a filter it can add, change or remove is a declaration in that file. A given the model already declares and reads is imported by name, never declared again; if it is only a reader's filter, move it into the report instead (see "Givens that stay in the model").
4. **COMPOSE THE FILE** at its path (the package root unless asked otherwise), following the template below, but do not save it yet. Name the sources you need in the import. Each tile binds its controls with a refinement, `view: t is v + { where: category ~ $CATEGORY }`, one clause per given it answers to; a `filter<...>` binds with `~`, a plain `date` or `number` given is a value and binds with `>=`, `<=` or `=`. Only the givens some tile references become controls. Then import every source or query any referenced given names in a `suggest`; **a `suggest` naming a `query=` needs that query's own source imported as well**, because an import is not transitive.
5. **COMPILE IT** with your compile tool (`POST …/models/<path>/compile` over REST) at the path the file will have, with **`"scope": "file"`**: a report opens with an `import`, which the default `append` scope refuses, and an existing file compiled at `append` collides with itself. **`"scope": "file"` does not check tiles, drills or suggest queries.** To see those before you save, compile the same text at `"scope": "package"` with `modelPath` set to the report's path: it runs the lint step 6 reads and returns its findings with code `dashboard-lint` (or `render-tag`). Editing an untagged model several reports import also wants `"scope": "package"`, which recompiles every file as saved.
6. **SAVE IT, RELOAD, AND READ THE MANIFEST AND THE WARNINGS.** Save by writing the file, or with `PUT …/models/<path>` over REST (omit `expectedHash` to create; it creates at any new path and replaces only a file the package already serves as a report). Then your reload tool, or `GET …/packages/<pkg>?reload=true`. A 424 means the package did not load and your edit is not live. **The `warnings` key is absent when there are none.** Then read `GET …/packages/<pkg>/reports/<path>`: its `givens` are exactly the controls that will render, and its `layout` is the layout Publisher read. See "Read the lint".
7. **OPEN IT AND LOOK.** Not optional; see "What done means".

## The form

```malloy
##! experimental.givens
##" One line saying what this report is for.
##| artifact { layout=grid title="Storefront overview"
  tiles=[
    intro { kind=text colspan=12 },
    "overview -> kpis",
    "overview -> revenue_trend",
    "overview -> revenue_by_state"
  ]
} dashboard { columns=12 }
|##
import { order_items, products } from "storefront.malloy"

# label="Category" control=select suggest { source=products dimension=category }
given: CATEGORY :: filter<string> is f''
# label="Ordered since"
given: SINCE :: date is @2023-01-01

##|(markdown) intro
Revenue and margin for the selected category.
|##

source: overview is order_items extend {
  # colspan=12
  # label="Key figures"
  # big_value
  view: kpis is key_figures + { where: category ~ $CATEGORY, where: created_at >= $SINCE }

  # colspan=8
  # break
  # label="Revenue by month"
  view: revenue_trend is sales_by_month + { where: category ~ $CATEGORY, where: created_at >= $SINCE }

  # colspan=4
  # label="Revenue by state"
  view: revenue_by_state is sales_by_state + { where: category ~ $CATEGORY, where: created_at >= $SINCE }
}
```

The same file with `layout=linear` and no `dashboard { … }` or `colspan` is a linear report.

- **The tag is model-level**, one line (`## artifact { … }`) or a `##|` … `|##` block with the same text inside, the form to use once the list is long: one tile per line, `}` on its own line, `|##` closing at the opener's column. A one-line `##` tag must stay on one line: wrapping it is a compile error, or worse, compiles clean and quietly stops being a report.
- **The header** above the tag holds only `##!` flags, `//` comments and the unnamed `"` notes that are the report's description. Keep statements and `#` tags below `## artifact`: nothing refuses one above it, but the header is where readers and the builder look.
- **Quote every query entry.** `"overview -> kpis"` is a string; unquoted, the tag does not parse (`notebook-artifact-unparsed`). A bare quoted name (`"top_products"`) names a model-level `query:` in the file.
- **A query tile is a thin `view:` on an extension the report declares** (`source: overview is order_items extend { … }`): the modelled view keeps its chart tag, the wrapper carries the tile's `# label` and its binding. On a grid its width, row break and height can go on its entry instead, `"overview -> kpis" { colspan=12 rowspan=2 }`, which wins over the view's tags; `rowspan=N` is rows of 80px, and `row=N col=N` place the tile at a grid cell (a report with any `row` is placed: fixed rows, tiles where they say, empty cells allowed).
- **A text tile is a `##|(markdown) name` block** listed as `name { kind=text }`. The name is one bare word on the opener line, the body starts on the next line (a heading goes inside the block), and `|##` closes it at the opener's column. A bare `intro` without `{ kind=text }` is the query-tile form for a `query:`, so the block is not shown. On a grid the entry also takes `colspan`, `break` and `rowspan`, and nothing else.
- **File order:** header, imports and givens, the prose blocks in tile order, then the extension that defines the tile views. That is the order the builder writes.
- **A `run:` in a report that lists `tiles` is never shown** (`notebook-layout-run`). Define a view, list it, delete the `run:`.
- **A tile expression is a string, so the compiler never checks it.** Rename a view and the file still compiles; the tile fails at package load, where the lint names it.
- **Chart tags go on the view** (`# line_chart`, `# bar_chart`, `# big_value`, `# label="…"`). `skill:malloy-charts` covers choosing them.

## Filter controls

Controls come from the `given:` declarations the tiles reference. The tags on the declaration are the control contract:

```malloy
##! experimental.givens

# label="Category" control=select suggest { source=products dimension=category }
given: CATEGORY :: filter<string> is f''

# label="Brand" control=multiselect suggest { query=brand_suggest dimension=brand }
given: BRAND :: filter<string> is f''

# label="Minimum line total" range_min=0 range_max=250
given: MIN_SALE :: filter<number> is f''

# label="Ordered since"

From malloydata/publisher, MIT.

More from malloydata/publisher

Signals

Stars
118
Forks
31
Open issues
106
Last commit (this folder)
Oct 18, 2026 (13 days ago)
Last release
Oct 18, 2026 (13 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from malloydata

Done for you

Rather not set it up yourself?

Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.

See how we build AI agents →Read the case studies

Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.

launches shipped
50+
launches shipped
media placements
500+
media placements
founders on every account
2
founders on every account

Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block

Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.

Get malloy-reports running in your business

Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.

Goes to the Chalk Labs founders, not the newsletter. Privacy

Need help deploying malloy-reports?

CallGet help
Cite this page

malloy-reports skill. Agent Mart. https://agentmart.dev/skills/malloydata-malloy-reports (last updated 2026-10-24)