Matplotlib
zLanqing/codex-claude-academic-skills
Low-level plotting library for full customization. An agent skill from zLanqing/codex-claude-academic-skills.
Build or modify a Malloy Publisher dashboard, a tagged .malloy file in a package's dashboards/ directory, with auto-rendered filter controls, a grid layout, and drill click-through.
$ npx skills add malloydata/publisher --skill malloy-dashboards -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install malloydata/publisher malloy-dashboards --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/malloydata/publisher.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/malloy-dashboards .claude/skills/malloy-dashboards && rm -rf skills-srcUse ~/.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/
Install the "malloy-dashboards" agent skill from https://github.com/malloydata/publisher/tree/main/skills/malloy-dashboards into .claude/skills/malloy-dashboards/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "malloy-dashboards", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/malloydata/publisher/tree/main/skills/malloy-dashboardsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add malloydata/publisher --skill malloy-dashboards -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install malloydata/publisher malloy-dashboards --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/malloydata/publisher.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/malloy-dashboards .agents/skills/malloy-dashboards && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "malloy-dashboards" agent skill from https://github.com/malloydata/publisher/tree/main/skills/malloy-dashboards into .agents/skills/malloy-dashboards/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "malloy-dashboards", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add malloydata/publisher --skill malloy-dashboards -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install malloydata/publisher malloy-dashboards --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/malloydata/publisher.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/malloy-dashboards .cursor/skills/malloy-dashboards && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "malloy-dashboards" agent skill from https://github.com/malloydata/publisher/tree/main/skills/malloy-dashboards into .cursor/skills/malloy-dashboards/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "malloy-dashboards", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/malloydata/publisher.git --path skills/malloy-dashboards--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add malloydata/publisher --skill malloy-dashboards -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install malloydata/publisher malloy-dashboards --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/malloydata/publisher.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/malloy-dashboards .gemini/skills/malloy-dashboards && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "malloy-dashboards" agent skill from https://github.com/malloydata/publisher/tree/main/skills/malloy-dashboards into .gemini/skills/malloy-dashboards/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "malloy-dashboards", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install malloydata/publisher malloy-dashboardsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add malloydata/publisher --skill malloy-dashboards -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/malloydata/publisher.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/malloy-dashboards .github/skills/malloy-dashboards && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "malloy-dashboards" agent skill from https://github.com/malloydata/publisher/tree/main/skills/malloy-dashboards into .github/skills/malloy-dashboards/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "malloy-dashboards", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add malloydata/publisher --skill malloy-dashboards -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install malloydata/publisher malloy-dashboards --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/malloydata/publisher.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/malloy-dashboards .opencode/skills/malloy-dashboards && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "malloy-dashboards" agent skill from https://github.com/malloydata/publisher/tree/main/skills/malloy-dashboards into .opencode/skills/malloy-dashboards/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "malloy-dashboards", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
malloy-dashboardsBuild or modify a Malloy Publisher dashboard, a tagged .malloy file in a package's dashboards/ directory, with auto-rendered filter controls, a grid layout, and drill click-through.
Malloy Dashboards is an agent skill from malloydata/publisher. Build or modify a Malloy Publisher dashboard, a tagged .malloy file in a package's dashboards/ directory, with auto-rendered filter controls, a grid layout, and drill click-through. Use when the user asks for a dashboard, a filterable operational view, or drill-through between views, and no code is wanted.
Its SKILL.md is about 8.4k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Data & Analytics. 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.
7 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit b9a1a19. It shows what the files ask for, not the result of running them.
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.
No scripts in the folder and no shell commands in SKILL.md (its code samples are malloy).
From the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Malloy Dashboards loads about 8.4k tokens when it runs. Until then it costs about 82 tokens; SKILL.md has 5,026 words of instructions outside code blocks.
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.
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.
The full file from malloydata/publisher at commit b9a1a19, republished under its MIT licence (© malloydata). 5,026 words, ~8,376 tokens.
.claude/skills/malloy-dashboards/SKILL.md (or your agent's skills folder).<!--
Copyright (c) Credible Data Inc.
SPDX-License-Identifier: MIT
-->
A
dashboards/*.malloyfile is a dashboard. It imports the model, names the views to show in## artifact { tiles=[…] }, applies the filtering on those views, and tags the layout. Publisher discovers it at package load, renders the filter controls from the givens it references, offers# drillclick-through between pages, and serves it at/<env>/<pkg>/dashboards/<name>. No code, no build step.
| The user wants | Use |
|---|---|
| A recurring, at-a-glance view behind shared filters | this skill (a dashboard) |
| A narrative, with prose between the numbers | a notebook (skill:malloy-notebooks) |
| Custom design, branding, or interactions beyond tags | an HTML data app |
| The model itself: sources, measures, joins | malloy-model |
Notebooks and dashboards run the same engine, so interactivity is not the axis: both get filter
controls, URL-addressable state, Apply batching, and # drill. Pick on the shape of the document.
Scanned at a glance is a dashboard; read top to bottom is a notebook.
Tool names below are bare (get_context, compile_model, reload_package). Match each against the
tools you actually have: a host may prefix or rename them. Where none matches, use the REST
endpoint named beside it.
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
dashboard; 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. A source the dashboard file declares on top of an exported one works;
one on top of a hidden source does not. See "Read the lint" for the warning you get otherwise.## artifact { tiles=[…] } naming existing views, so
this is the design step: which views, how wide each sits, what each is called.dashboards/, following the template below, but do not save it yet.
Name the sources you need: import { order_items, products } from '../storefront.malloy'.
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, so a declaration nothing binds shows nothing. Then import every
source or query any referenced given names in a suggest. Both are per-file, and getting the
suggest wrong does not error: the control still looks like a picker but has no options, and says
so underneath, "Could not load the options for this control". The package warnings name it too. A suggest naming a query= needs that
query's own source imported as well, because an import is not transitive: the query resolves by
name, so the file compiles and the package loads, but running the picker fails with
Undefined source '<name>'. The package warnings name this one too, saying which source to
import. Import the source that suggest query reads, not just the query.POST …/models/<path>/compile over REST), against the source text,
before you save, at the path the file will have. Editing one that already exists needs
"scope": "file", which compiles your source AS that file; the default appends it instead, so
every imported name and the query name collide with the saved copy and you get a wall of
already-defined errors that reads as broken Malloy rather than a wrong scope. Editing a shared
include wants "scope": "package", which recompiles every file as saved: file only checks the
one you are editing, so renaming a source in _shared.malloy passes it while breaking every
dashboard that imports it. "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 dashboard's path: it runs the lint step 6 reads and returns its findings with code
dashboard-lint (or render-tag), each at the severity a load gives it, so an error finding
makes the compile an error too. Even that is not a working dashboard: some mistakes only show when
you look at the page in step 7. A not-yet-saved dashboard wants
"scope": "file" too, not the default: a dashboard file opens with an import, and the
default append scope refuses one, so the new-file case fails on the import and again on the
path that does not exist yet. append is for a fragment checked against a model that is
already on disk, which a dashboard file is not.GET …/packages/<pkg>?reload=true over REST. Check the status the reload returns as well as the warnings:
a 424 means the package did not load and your edit is not live. The warnings key is absent
when there are none, so an empty response is the pass, not a sign you are reading the wrong
field. Then read GET …/packages/<pkg>/dashboards/<name>: its givens are exactly the controls
that will render, which catches a given you imported but never referenced before you open the page,
and its query is the name to run in step 7. See "Read the lint" below.A dashboard is ## artifact { tiles=[…] } at model level: named views, each run as its own query, laid
out by Publisher into the grid # dashboard { columns=N } names.
##! experimental.givens
## artifact { title="Storefront overview" tiles=["overview -> kpis", "overview -> revenue_trend", "overview -> revenue_by_state"] } dashboard { columns=12 }
import { order_items, products } from '../storefront.malloy'
// The controls, declared here: the tags are each one's control contract.
# 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
// Layout goes on the VIEW, and a thin re-declaration is the place to put it: the
// modelled view keeps its chart tag, this decides how wide it sits here, and the
// `+ { where: ... }` says which controls the tile answers to.
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 }
}Model-level because there is no query of its own to hang a # tag on, and model-level for a second
reason: tiles run as separate queries, which is the only way a page can span unrelated sources. A
nest's pipeline starts from its own query's source and there is no way to combine two.
Three things the form costs, so you are not surprised by them:
compile_model at "scope": "package" (step 5).+ { where: ... } names the
controls it answers to. Below is why, and the one thing that does not work.Prose between tiles is a named (markdown) block that tiles= lists with kind=text; it renders as a tile of its own, and its body is markdown:
## artifact { title="Storefront overview" tiles=[intro { kind=text colspan=6 break }, "overview -> kpis"] } dashboard { columns=12 }
##|(markdown) intro
## How to read this page
|##The entry is name { kind=text colspan=6 break }: colspan and break lay it out like a query tile, and nothing else on the entry is read. The name is one bare word on the opener line, the body starts on the next line, and the |## closer sits at the opener's column. A heading goes inside the block, because a bare ## Heading line is a model tag. Keep the parentheses: ##|markdown draws a malformed-route warning. The lint reports an entry with no block, a block written twice, and a (markdown) block that no tiles entry names, so delete the last rather than leave it. The dashboard's own description is separate: the unnamed " notes above ## artifact. Earlier spellings are still read: ##|(text) name is a text tile, like ##|(markdown) name. Write (markdown).
In the API a text tile has kind: "text", a name and markdown, and no query; code that runs a dashboard's tiles skips it.
# artifact on a query:An older package may carry a single query tagged # artifact whose result is the whole page, laid out
by @malloydata/render from the query's own # dashboard tag. It still renders. Do not author a new
one, and do not convert an existing one unless asked: edit it minimally, and when it has to change
shape, rewrite it as tiles=[…]. It cannot span sources, and a guessed field inside it fails the whole
package load, where a bad tile in tiles=[…] fails alone, in its own cell. "Legacy single-query pages"
near the end covers the few traps it has.
A dashboard has no query, so its filtering lives on the views it names: a + { where: field ~ $GIVEN }
refinement on each tile, one clause per control the tile answers to. A tile without the clause does
not move when the control does, which is how a page keeps one tile fixed while the rest filter. The
givens those clauses read are declared in the same file (step 3).
Binding is per declaration, not per name. Measured: a dashboard declaring its own CATEGORY
over a source whose model-level where: reads the model's CATEGORY compiles, shows the control,
and filters nothing when it moves, because the two declarations only share a name. So do not mix
the two designs on one given. The other design still works on its own: a source with the givens
already applied in an untagged dashboards/_shared.malloy, reading import '../givens.malloy',
which discovery treats as a shared include rather than a dashboard. Then the dashboard imports both
and the controls render for the givens its tiles reach. A given the dashboard imports but nothing
references gets no control, silently, at reload 200 with no warning. Save the include before you
compile the dashboard that imports it, since an importer compiled against a sibling that is not on
disk fails with an import-error. The builder can bind such a control but not add, change or
remove it, since it never edits imports or model files.
Note SINCE is a date rather than a filter<>, so it compares with >= rather than ~.
A given stays in the model, declared once there, when any of these reads it: a model source, view or
measure; an #(authorize) or #(access_filter) gate; or an HTML data app. The dashboard imports it by
name (import { CATEGORY } from '../givens.malloy') and never re-declares it. Getting this wrong fails
in two different ways:
where: or gate never sees the value, so the filtering silently does nothing.Declare locally only what no model code reads: a control that exists for this page's tiles. A drill
into this page seeds a given by name, so the page declares that same name as filter<T>, with the
type the clicked value fits (filter<string> for a category).
A composite declares in its own file any source it scopes itself, such as
source: overview is order_items extend { … } above, so the givens its tiles bind are in the same file
that binds them.
# dashboard { columns=N } is the spelling of the grid width, beside the artifact tag. dashboard_columns=N inside the artifact tag is a deprecated alias: it is read when
columns is absent and draws a warning, and when the two disagree it is an error naming both values.
Any other property inside the artifact tag that Publisher does not read is a package warning naming it.
A tile keeps its view's own field names on axes and column headers. # label titles the tile; to label
what is inside it, label the fields in the view.
Cards and tiles share one grid. Publisher reads these tags off the view each tile in tiles=[…]
names. Copy this recipe and use the same count on every dashboard in the package so they read as
one product:
columns=12 on the # dashboard tag. Twelve divides by 2, 3, 4 and 6, so a row is even with
three cards or four.# colspan on every card and tile, summing to 12 per row. Four cards at 3, three at 4, two
tiles at 6, a full-width table at 12. Omit them and every item falls to a single column, a twelfth
of the width, which is too narrow for a line chart to draw in at all. A colspan wider than
columns is clamped, and the package warnings say so.# break on the first tile after the cards. Otherwise it flows into the columns left beside
the cards and the next tile wraps. Not needed per row: once a row sums to 12 the next item wraps
on its own.# label="..." on every tile view and aggregate, including the aggregates inside a table
view, whose column headers are field names too. The heading is otherwise derived from the view's or
field's name, and a wide table full of total_sales and order_item_count is the most visible
thing between a rough page and a finished one. A view referenced by name is the exception: you
can label the tile, but its own field names still reach the chart axes and the column headers, so
label the fields in the view if you want those too.On a dashboard, all four go on the view a tile names, which is why a thin re-declaration
(view: revenue_trend is sales_by_month) is the place to put them: the modelled view stays reusable
and each page decides its own widths. # subtitle="..." and # borderless go there too and are the
rest of the set; a tile reads all five.
Do not forget columns= itself. Without it the grid falls back to a narrow default and every
colspan is clamped to it, which is the usual reason a page you laid out comes out one item per row, and
the package warnings name it.
Then the traps:
# big_value on the view says so
explicitly; # table opts out).# size=fill on a dashboard tile. Inside a dashboard it measures against the container the
whole grid was handed, not the tile, so it yields a chart thousands of pixels tall. Tiles already
size to their colspan.# table { size=fill } on the view forces it. That is the table's own tag, and it is a
different thing from the # size=fill above, which is the chart one this page warns against.order_count / customer_count renders as 10.695 on a card;
# number="#,##0.0" is the precision it actually carries.# shape_map legend is titled with the measure's field name, not its # label. Rename it
in the view: aggregate: revenue is total_sales. Renaming drops the measure's own format tags
though, so # currency becomes plain digits unless you re-tag it at the rename. Prefer # label=
anywhere the legend is not the problem.20…; # label="Order year"
instead of "Year" buys the room. A legend showing … is this, not a data problem.The last two are upstream renderer behavior, cheap to work around in the model.
Height is the one thing the surface decides: a dashboard's tiles are each capped, and in a notebook a chart cell is capped and a table cell hugs its rows.
# drill
target. The query inside can be called anything, and sometimes must be (a query named regions
collides with an imported regions source).where: that references it lives up an import
chain. And a given declared here does not drive a where: in the model: bind on the tiles.suggest { source=products … }
means the dashboard imports products.## tag must be on one line. Wrapping one always breaks it, but how you find
out depends on what follows. If the continuation is not valid Malloy you get a compile error. If it
happens to be, an import say, the file compiles clean, quietly stops being a dashboard and
becomes a shared include, and only the package warnings tell you. Match on the shape rather than the
words: the message may say a tag "does not parse", or was "refused" or "dropped rather than parsed",
and on a file that still built it opens "Annotation" rather than "Tag". See "Legacy single-query pages" below.skill:malloy-charts covers the renderer tags and choosing them.
Controls come from the given: declarations the tiles reference. Declare the new ones in the
dashboard file, which is what the builder edits; a given the model already declares is imported by name
and gets the same control, which the builder can bind but not edit.
The tags on the declaration are the control contract:
##! 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"
given: SINCE :: date is @2023-01-01A sixth tag, description, is part of the contract and has two spellings that do different things:
# description="…" publishes to the API but Publisher's own UI does not render it, while
#(description="…") renders as helper text under the control but complains about any multi-word
value, that the prefix "is not a well-formed route", because a route ends at the first space. That
complaint is a compile diagnostic on a compile that still succeeds, not a package warning, so
step 6 will not show it. Pick by which reader you care about.
dimension= can name a field of a directly joined source (one join level). Quote it, because
an unquoted dotted value does not parse as a tag:
suggest { source=sales dimension="products.department" }.
On the query= form of suggest, dimension names a column of that query's output, not a path into
the model, and only the last segment of it is read. Match it to the suggest query's group_by. When it
matches no column the picker falls back to the first column of the result: silently right for a
one-column suggest query, silently wrong for a multi-column one.
control=select/multiselect with a suggest renders a picker filled from the data;
range_min/range_max on a filter<number> renders a two-handled range slider; a filter<date> or
filter<timestamp> renders a time-range control with preset windows and a custom day range; a bare
date or timestamp renders a date picker. Which controls appear is per-dashboard, decided by which
givens the query references.
malloy-model (a modeling skill) and docs/givens.md cover givens themselves.
Two per-dashboard options on the artifact tag:
autorun=false batches control changes behind an Apply button. Add it once a page is slow enough
that a reader notices two round trips.givens { CATEGORY=f'Outerwear' } sets starting values, not a redeclaration. A URL parameter wins.A served .malloy notebook takes both inside its ## artifact { … } tag, as autorun=false and
givens { CATEGORY=f'Outerwear' }, and behaves identically. A file-level ## autorun=false does
nothing there; only a legacy .malloynb reads options at the file level.
# drill goes on a model dimension, never on a dashboard:
# drill { to=["category", "self"] given=CATEGORY }
dimension: category is products.categoryto=<slug> navigates to that dashboard with the clicked value written into the named given;
to=self filters in place; two or more destinations pop a menu.
Declaring it on the dimension is the point: every result that groups by it becomes clickable, in a
dashboard tile and in a notebook cell alike. So when a view is meant to be drilled, group by the
tagged dimension. Declaring dimension: category is products.category and grouping by category
gives the identical output field name and the identical numbers, and carries the tag.
Always write given=. Without it the given name is the dimension name verbatim, so a
dimension: category seeds a given called category rather than a declared given: CATEGORY. A
to=self survives that, because a surface folds case when it looks up its own given. A to=<slug>
does not: it navigates, still looks like it worked, and arrives as ?category=…, which the
destination drops by exact match, so you land on an unfiltered page. Nothing errors, and the lint
folds case when it checks, so it stays green too. That silence is specific to a name that folds onto
a declared given. A to=self whose name matches nothing at all is caught loudly and is not offered;
a to=<slug> is not checked either way.
A drill only lands somewhere useful if the destination declares a control for the given being
seeded. No lint checks that. It verifies that the target slug is a dashboard in the package, and
for to=self that some model declares the given, and stops there. Nothing reads the destination's
own givens, so click it and look.
Cells in a drillable table column show it: pointer cursor, and a blue underline on hover. They are
in the tab order and carry a button role too, so a keyboard reaches them, focus is styled the way
hover is, and Enter or Space fires the drill. Chart marks get no such affordance in either Publisher
or Malloyyo, so a dashboard meant to be drilled wants at least one untagged (table) tile. A destination the
surface cannot honor is not marked and not offered, which is why a to=self reads as plain text in a
document that declares no control for its given.
One thing quietly switches the marking off, per column: another tile rendering a column with the
same header. Put a "revenue by category" chart beside a drillable category table, which is the
obvious thing to build, and that column's cells stop being marked. Marking matches columns by their
rendered header text, so a name a non-drillable field also shows is dropped rather than risk painting
a dead link. Only a non-drillable column suppresses: a second tile that groups by the same tagged
dimension is fine, which is the arrangement the paragraph above already recommends. Other drillable
columns on the same page keep their marking, so counting marked cells will not tell you: look at the
column you care about. The clicks still work, so this is invisible
unless you hover. Give the two different headings with # label=. A transposed table is never marked
either, for a different reason.
Only for a package that already has one (see "Legacy" above). Its layout can come out wrong in two
visibly different ways, and the reload is 200 and the manifest reports the column count you asked for in
both. A top-level aggregate: there is the KPI card; nest views for tiles; give each a # colspan and
the first tile after the cards a # break; # dashboard { gap=N table { max_height=N | none } } are
options only this form reads.
Not a dashboard at all: one plain nested table, every # colspan and # break dropped, because a
f'…' filter literal in a givens { … } block shares a line with # dashboard. Put # dashboard on
its own line.
A dashboard, but nothing lines up: # colspan without columns=N, so the items flow side by side
at their natural widths instead of aligning to a grid.
To tell them apart, run the page's own query, {"queryName": "<the manifest's query>"}, and read
renderLogs on the response (absent when there is nothing to say):
| render log | what it means |
|---|---|
Unknown render tag 'colspan' | the renderer never saw a # dashboard tag. It does not say which of the two causes; check both |
Ignored # colspan … only applies in columns mode | it saw the tag but there is no count |
Neither reaches the package warnings. A wrapped ## tag is the one failure in this family that does:
the file is absent from the listing and the package warnings say "Tag does not parse (Unclosed '{')".
Package warnings after a reload are the dashboard's test suite. Fix all of them:
# drill … targets "x", which is not a dashboard in this package: a dead click.to=self, but no model in this package declares a given "X": the clicked value has nowhere to go.given "X" suggests options from source "y", which this file does not define: the dropdown will
be empty, so import it.filters by given "X", which this file does not import, so no control is shown for it: the trap
under "Importing a given is what makes it bindable", which the lint now names for you, with the file
to fix.# dashboard { columns }; a tile view's
# colspan that is not a positive integer or is wider than the grid; a dashboard_columns= alias,
which is an error when it disagrees with dashboard { columns }; and any property inside the
artifact tag Publisher does not read.Findings carry a severity, but warn is the ordinary default and tells you nothing about how bad
one is. Read the text, not the severity and not the count. One message is worth recognising because
it changes what the rest of the list means: "Dashboard lint stopped early, so this list is
incomplete".
Read the status the reload itself returns, not the listing. One dashboard that fails to compile fails the whole package load, and the reload answers 424 with the compile error. A package that was already serving then keeps serving its previous version, so the listing still answers 200 and looks perfectly healthy while your edit has silently not taken effect.
If you did not catch the 424, GET /api/v0/status still knows. A package serving an older model
than its files appears in loadErrors with stale: true, the compile message, and the time it
failed, and the entry clears on the next reload that compiles. That is the one check that works after
the fact, so make it the first thing you run when a page will not change. A package that never loaded
at all appears there too, without stale, and is absent from the listing entirely.
If the reload is 200 and the others are listed but yours is not, discovery skipped the file,
usually a missing or misspelled # artifact tag. That is the same mechanism that deliberately skips
an untagged shared include, and it is also how you hide a dashboard on purpose: drop its tag. The
package surface never hides a dashboard. Every tagged dashboard is listed and served. Dropping the tag
hides it as a dashboard, but not always as a file: with no index.malloy, or with a legacy explores
that lists it, the untagged file is then listed and published as an ordinary model.
A tile over a hidden source answers 404. When the package has an index.malloy, tiles, a
legacy single-query page's query, and filter suggest lists read only the sources it exports. The
dashboard file admits nothing of its own, so importing a source into it is not enough. The package
load warns once per tile that will fail, for example:
Tile orders_staging -> by_flag on dashboard overview reads orders_staging, which index.malloy doesn't export, so it won't load. Fix: add orders_staging to the export { ... } in index.malloy.The fix is the one the warning names: add the source to the export { ... } in index.malloy. In a
package that gates a source with #(authorize) or #(access_filter), the warning says "a source"
instead of naming it. A
suggest over a hidden source gets the same warning, ending "so its list will be empty". Do not
add an explores to publisher.json to serve a dashboard; it is deprecated and no longer needed.
A clean reload is not proof the tags are right. The checks above read names and resolve them; the separate warning for a tag that does not parse is syntax only: it carries no position and says nothing about a name that does not resolve. It catches a malformed tag; its absence is not evidence there are none. That is why the last step is opening the page, not reading the warning list.
# drill, you clicked it and landed where you meant to, with the given seeded.columns, and the rows end flush
with each other. Nothing is clipped, no tile is thousands of pixels tall, and no legend or card
label ends in ….docs/dashboards.md: the full guide this skill condenses.docs/givens.md: the givens the controls are generated from.© malloydata, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/malloy-dashboards of malloydata/publisher.
Open the folder on GitHubat commit b9a1a19
Malloy Dashboards 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.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Malloy Dashboards this skillmalloydata/publisher | 116 | — | ~8.4k | Automated safety check: Pass | MIT | |
| MatplotlibzLanqing/codex-claude-academic-skills | 4.7k | 17 repos | ~2.9k | Automated safety check: Pass | MIT | |
| Exploratory Data Analysisspacering-net/codeg | 3.9k | 14 repos | ~3.6k | Automated safety check: Pass | MIT | |
| Scikit LearnzLanqing/codex-claude-academic-skills | 4.7k | 16 repos | ~3.9k | Automated safety check: Pass | BSD-3-Clause | |
| Chart Visualizationbytedance/deer-flow | 84k | 2 repos | ~840 | Automated safety check: Pass | MIT | |
| TimesFM Forecastinggoogle-research/timesfm | 34k | — | ~4.7k | Automated safety check: Pass | Apache-2.0 |
zLanqing/codex-claude-academic-skills
Low-level plotting library for full customization. An agent skill from zLanqing/codex-claude-academic-skills.
spacering-net/codeg
Perform comprehensive exploratory data analysis on scientific data files across 200+ file formats.
zLanqing/codex-claude-academic-skills
Machine learning in Python with scikit-learn. An agent skill from zLanqing/codex-claude-academic-skills.
bytedance/deer-flow
Picks a suitable chart type from 26 options for your data, maps the data to that chart's parameters and generates a chart image through a JavaScript script.
google-research/timesfm
Forecasts any univariate time series zero-shot with Google's TimesFM model, returning point forecasts and calibrated prediction intervals without training.
vercel/next.js
Benchmark React or Next.js changes on Vercel Sandbox VMs with paired A/B statistics: react PR/commit vs base, or Next.js PR/commit vs base, measured end-to-end through the bench/render-pipeline app…
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.
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…
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.
malloydata/publisher
Conduct a local Publisher evaluation loop in five steps: scrape/run, eval, diagnose, improve, checkpoint.
malloydata/publisher
Make the smallest safe Malloy model edit that closes a diagnosed model-owned gap, with a probe receipt for every factual claim.
malloydata/publisher
Decide whether ONE answer matches its golden, and say whether you believe the golden.
Categories
Build or modify a Malloy Publisher dashboard, a tagged .malloy file in a package's dashboards/ directory, with auto-rendered filter controls, a grid layout, and drill click-through. Malloy Dashboards is an agent skill from malloydata/publisher.malloy file in a package's dashboards/ directory, with auto-rendered filter controls, a grid layout, and drill click-through.
Malloy Dashboards fits situations like: the user asks for a dashboard; A filterable operational view; drill-through between views; no code is wanted.
Run `npx skills add malloydata/publisher --skill malloy-dashboards -a claude-code`. Or copy the skill folder (skills/malloy-dashboards in malloydata/publisher) into .claude/skills/malloy-dashboards in your project. Claude Code loads it when a task matches its description.
Run `npx skills add malloydata/publisher --skill malloy-dashboards -a codex`. Or copy the skill folder (skills/malloy-dashboards in malloydata/publisher) into .agents/skills/malloy-dashboards in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add malloydata/publisher --skill malloy-dashboards -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-dashboards, .gemini/skills/malloy-dashboards, .github/skills/malloy-dashboards and .opencode/skills/malloy-dashboards in your project.
SKILL.md names no scripts, command-line tools or credentials: Malloy Dashboards is instructions for the agent only.
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.
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.
Malloy Dashboards is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 8.4k tokens (SKILL.md is roughly 34k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Malloy Dashboards: Matplotlib (zLanqing/codex-claude-academic-skills, 4.7k stars), Exploratory Data Analysis (spacering-net/codeg, 3.9k stars), Scikit Learn (zLanqing/codex-claude-academic-skills, 4.7k stars) and Chart Visualization (bytedance/deer-flow, 84k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
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 9, 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.