End-to-end BI development tool for Metabase: design dashboards, write and test SQL, keep dashboards as files, publish them, look at the result, and share or schedule it.
All scripts are in ${CLAUDE_SKILL_DIR}/scripts/ and need only Node.js (no npm install; rendering needs Node 22+ and
Google Chrome). Every command below is node ${CLAUDE_SKILL_DIR}/scripts/metabase.mjs <command>.
First-Time Setup
Run instances to see which Metabase connections exist (it never prints keys or tokens). Never read
~/.metabase-skill/config.json or anything under ~/.metabase-skill/tokens/ — they can hold secrets.
- Instances listed: use them (ask which one when there are several).
statusshows who you're signed in as, how, the Metabase version, and whether it's the free edition. - None configured: ask the user to sign in, in their terminal (it opens a browser; you can't do it for them):
node ${CLAUDE_SKILL_DIR}/scripts/metabase.mjs login --instance <name> --url https://metabase.example.com
Browser sign-in (Metabase v63+) uses the person's own Metabase account and permissions; tokens go to the macOS
Keychain (a private file elsewhere) and renew automatically. API keys still work for scripts and CI: setup →
env or direct mode. If login says app registration is turned off, an admin disabled Admin > AI > MCP (which also
turns off automatic registration): re-enable it, or register an app and pass --client-id.
Quick Reference
Global options (every command):
--instance <name>— use a connection other than the default--json— JSON output (list/discovery commands print text tables without it; get/create/update always print JSON)--fields <a,b.c>— only these dot-paths of the JSON result (per item on lists), e.g.card 402 --fields name,dataset_query.stages.0.native--max-bytes <n>— cap printed JSON (default 24576,0= none). Lists are trimmed with a notice; an oversized object is refused (exit 2) — narrow it with--fieldsor use--out--skip-preflight— try a Pro/Enterprise-only command on an instance without the feature--theme light|dark— colour scheme for Chrome-rendered pictures (default light; Metabase otherwise follows the OS)
Exit codes: 0 ok, 1 operation failed, 2 input/pre-flight problem (fix the input; the message says what).
Sign-in
| Command | What it does |
|---|---|
login --instance <n> --url <u> [--client-id <id>] |
Browser sign-in (v63+); creates the connection |
logout [--instance <n>] |
Revoke and delete the stored sign-in |
status [--instance <n>] |
User, auth method, token expiry, Metabase version, edition |
setup / instances |
Wizard (browser or API key) / list connections |
Projects — a dashboard as YAML + SQL files (see specs/project-format.md)
| Command | What it does |
|---|---|
project new <dir> --name <n> --database <id> [--collection <id>] |
Empty project folder for a new dashboard |
project pull <dir> [--dashboard <id> [--loose-cards] | --collection <id>] [--force] |
Metabase → files (first pull needs --dashboard or --collection; --loose-cards also keeps the collection's cards that are on no dashboard; refuses to overwrite local edits) |
project validate <dir> [--json] |
Structure, references, 24-column grid, filter wiring, SQL tags, chart settings |
project lint <dir> [--strict] [--offline] |
Design checks (descriptions, number formats, full rows, unwired filters, …) |
project diff <dir> [--json] |
Exactly what push would change, field by field and placement by placement |
project status <dir>... [--json] |
Every project at or under the folders: in sync, changed in Metabase (pull), or not pushed yet (push). Read-only |
project push <dir> [--dry-run] [--yes] |
Files → Metabase (snippets, cards, dashboard, subscriptions); warns on drift |
project test <dir> [--card <key>] [--images] |
Run every card (as in the files) with the dashboard's default filters; --images → PNGs |
project render <dir> [--pdf] [--tab <name>] |
Picture / PDF of the published dashboard → <dir>/.mbsync/render/ |
Pictures and PDFs (see specs/rendering.md)
| Command | What it does |
|---|---|
card image <id> [--out f.png|f.pdf] [--params <json>] [--width N --height N] |
The saved question's chart (card-level settings, no dashboard filters) |
dashcard image <dashboard-id> <index|name> [--id] [--out ..] [--params <json>] |
One tile as it looks on the dashboard: its own title, value, formatting, and the dashboard's filters |
dashboard render <id> [--out f.png|f.pdf] [--tab <name|id>] [--params <json>] |
Every tab (PNG each, or one PDF); reports cards showing errors |
dashboard render <id> --server [--out f.pdf] [--paper a4|letter] |
PDF drawn by Metabase itself (v63+, subscription style); no Chrome needed |
Discovery
| Command | What it does |
|---|---|
databases / tables --database <id> |
Databases / tables and fields |
collections [--tree] / collection-items <id> [--models card,dashboard] |
Collections (★ = official) / contents |
search <query> [--models ...] |
Find anything by name |
Cards (questions)
| Command | What it does |
|---|---|
card <id> |
Summary: result columns, snippets, template tags |
card <id> --full --out <file> / --layout --out <file> |
Full payload / writable fields, to a file |
card create --from <file> [--skip-validate] |
Create (checked first: tags, field filters, compile, chart settings) |
card update <id> --patch <file> [--skip-validate] |
Fetch → merge → check → save |
card delete <id> [--permanent --yes] / card restore <id> |
Archive (restorable) / hard delete / un-archive |
card copy <id> [--collection <id>] |
Duplicate |
card query <id> [--params '{"tag": value}'] [--out <file>] |
Run it (20 rows, or all rows to a file) |
card query <id> --export csv|json|xlsx --out <file> [--params ..] |
Download results |
card history <id> / card revert <id> --to <revision-id> [--yes] |
Who changed what / restore a version |
pivot build --table <id> --rows <f[:unit],..> [--columns ..] --values <count|sum:F,..> (--out f.json | --project <dir> --key <k>) |
Write a correct structured pivot question (run once to check it) |
Dashboards
| Command | What it does |
|---|---|
dashboard <id> |
Summary: tabs, filters, placements, repeated_card_ids, per-placement overrides |
dashboard <id> --layout --out <file> / --full --out <file> |
Layout only (~3K tokens for 30 cards) / full payload |
dashboard create --from <file> |
Create a shell |
dashboard put <id> --from <file> [--skip-validate] |
Save a whole layout (every referenced card is checked first) |
dashboard update <id> --patch <file> |
Fetch → merge → save |
dashcard <dashboard-id> <index|name> [--id] |
One placement's settings and filter wiring |
dashcard update <dashboard-id> <index|name> [--id] --patch <file> |
Change one placement; the rest untouched |
dashboard delete <id> [--permanent --yes] / dashboard restore <id> |
Archive / hard delete / un-archive |
dashboard copy <id> [--collection <id>] |
Duplicate |
dashboard history <id> / dashboard revert <id> --to <revision-id> |
Who changed what / restore a version |
Alerts and subscriptions (see specs/notifications-api-spec.md)
| Command | What it does |
|---|---|
alerts [--card <id>] / alert <id> |
List / show card alerts |
alert create --from <file> / alert update <id> --patch <file> |
Alert on a card (condition + Quartz cron + recipients) |
alert archive <id> / alert restore <id> / alert send <id> [--yes] |
Stop / resume / deliver now to real recipients |
subscriptions [--dashboard <id>] / subscription <id> |
List / show dashboard subscriptions |
subscription create --from <file> / subscription update <id> --patch <file> |
Email/Slack a dashboard on a schedule |
subscription archive <id> / subscription restore <id> |
Stop / resume |
Snippets, collections, metadata
| Command | What it does |
|---|---|
snippets / snippet <id> |
List / show SQL |
snippet create --name <n> --content <sql> [--collection <id>] [--description <d>] |
Create |
snippet update <id> [--name] [--content] [--description] [--archived true|false] |
Update or archive (no hard delete) |
collection create --name <n> [--parent <id>] [--description <d>] |
Create |
collection update <id> [--name] [--parent] [--description] [--archived true|false] |
Rename, move, archive |
database sync-schema <id> / field rescan-values <id> / field discard-values <id> |
Pick up new columns / refresh dropdown values |
Users, groups, permissions
users [--query --status --admins --group <ids> --groups-all <ids>], user <id>|create|update|deactivate,
groups, group <id>|create|delete|add-user|remove-user, permissions [--database] [--group] [--native-sql] [--collections], permissions summary, permissions set … [--dry-run] [--yes], permissions set-collection ….
Pro/Enterprise only (refused early on the free edition): usage-analytics …, sandboxes, sandbox create|delete,
permissions app [set], permissions set-snippets, collection … --authority-level official.
--help prints every command and flag.
Context Safety Rules
Not optional:
- Never load full dashboard/card payloads into context. Use the summaries (
card <id>,dashboard <id>),--fieldsfor one part, or--full --out <file>. - Never print or read secrets. API keys and tokens never appear in output; don't read
config.jsonortokens/. - Use files for anything you send.
--from/--patchtake file paths, in the project directory — never/tmp. - Look at pictures, not payloads, to judge a chart.
card image/dashboard render+ Read the PNG.
When to Ask the User
You are the builder; the user decides. Ask (AskUserQuestion) when choosing: the instance; the dashboard's purpose and
audience; metrics and dimensions; layout (tabs, arrangement, chart types); filters (which, defaults, cascading);
SQL vs MBQL when unclear; visual choices with several valid options; before creating or changing anything in
Metabase (show the plan or project diff); before any delivery to real recipients (alert send, new
subscriptions — confirm addresses); and whenever something fails unexpectedly (report it; don't silently retry).
Task Identification
| Mode | When | Workflow |
|---|---|---|
| NEW | Build a dashboard | Design → project files → validate/lint/test → push → visual check |
| EDIT (project) | Change a dashboard kept as a project folder | pull → edit files → diff → push → visual check |
| EDIT (direct) | Quick change to a dashboard not kept as files | STUDY → change via card/dashcard commands → visual check |
| STUDY | Understand a dashboard (describe, audit, plan changes) | Summary → render → per-card inspection → override audit |
| SHARE & SCHEDULE | PDF/PNG for someone, alerts, subscriptions | render / alert / subscription |
| EXPLORE / REORGANIZE | Browse, search, move, rename | Discovery and collection commands |
| HISTORY | "Who changed this", "put it back" | history → confirm → revert |
| USAGE-ANALYTICS | Adoption, stale content, query performance (Pro/Enterprise) | MBQL against analytics models |
| ADMIN | Users, groups, permissions | Permission commands |
A dashboard that already has a project folder (it has project.yaml and ids.json) is always edited through the
project: direct edits in Metabase make the files stale (push would warn about drift).
Reference Loading Strategy
After choosing the mode, read every spec in its bundle before starting. Deferred reads lead to broken payloads.
| Mode | Specs (${CLAUDE_SKILL_DIR}/specs/…) |
|---|---|
| NEW / EDIT / STUDY | project-format.md, rendering.md, dashboard-api-spec.md, card-api-spec.md, visualization-cookbook.md, sql-style-guide.md, snippet-api-spec.md, discovery-api-spec.md |
| SHARE & SCHEDULE | rendering.md, notifications-api-spec.md |
| EXPLORE / REORGANIZE | discovery-api-spec.md, collection-api-spec.md, card-api-spec.md |
| Standalone SQL / card / snippet | card-api-spec.md, sql-style-guide.md, snippet-api-spec.md, visualization-cookbook.md |
| USAGE-ANALYTICS | usage-analytics-spec.md |
| ADMIN | permissions-api-spec.md, permissions-guide.md |
Assets (${CLAUDE_SKILL_DIR}/assets/templates/): mockup-text-template.md, mockup-html-template.html,
design-doc.md — load at the design stage that needs them.
| Spec | Content |
|---|---|
project-format.md |
Project folder format, what's derived, push/pull behaviour, lint rules |
rendering.md |
How card/dashboard pictures and PDFs are made, options, output, limits |
notifications-api-spec.md |
Alerts vs subscriptions, bodies, schedules, traps |
dashboard-api-spec.md |
Dashboard CRUD, layout, parameters, mapping targets, PUT rules |
card-api-spec.md |
Card CRUD, MBQL, native SQL, template tags, what v63 returns |
visualization-cookbook.md |
Every display's required settings and sizes; copy-pasteable examples |
sql-style-guide.md |
SQL conventions, sample-database dialects |
snippet-api-spec.md / collection-api-spec.md / discovery-api-spec.md |
Snippets / collections / metadata and search |
permissions-api-spec.md / permissions-guide.md |
Users, groups, permission graph / recipes and safety |
usage-analytics-spec.md |
Analytics models, MBQL patterns (Enterprise) |
NEW Dashboard Workflow
Before you start
Ask: design first (recommended for 5+ cards, tabs or complex filters) or build directly (simple requests)?
Then agree the project folder (e.g. <repo>/dwh/metabase/<dashboard>/); never /tmp.
Phase 1: Design — Define → Mockup → Approve
Get explicit approval at each stage; never skip one silently.
Stage 1: requirements. The business question, the audience (exec: tiles and trends; analyst: tables and
drill-downs), the data (databases, tables --database <id>). Then define and present for approval:
| Metric | Calculation | Display | Card |
|---|---|---|---|
| Total Revenue | SUM(total) | scalar | Revenue KPI |
| Dimension | Source | Used in |
|---|---|---|
| Month | orders.created_at (month) | Revenue trend |
| Filter | Type | Default | Applied to |
|---|---|---|---|
| Date | date/all-options | past30days~ | all cards |
Don't continue until the user confirms — a wrong data model makes everything built on it wrong.
Stage 2: progressive mockup (approve each level):
- Text mockup (
assets/templates/mockup-text-template.md) →<project>/design/mockup.md: tile/chart/table arrangement on the 24-column grid. - HTML mockup (
assets/templates/mockup-html-template.html) →<project>/design/mockup.htmlfor the browser. - Metabase mockup: write the project with simple sample SQL (
SELECT 'Gadgets' AS "Category", 42000 AS "Revenue"or real queries withLIMIT),project push, thenproject render <dir>and show the user the PNG — they see the real layout, filters and formatting without clicking through Metabase.
Stage 3: design document (assets/templates/design-doc.md) → <project>/design/design-doc.md.
Phase 2: The project files
project new <dir> --name "Store Analytics" --database 3 --collection 17
Then write (format: specs/project-format.md):
snippets/<name>.sql— one feeder snippet per data source: the joins, the filters ([[AND {{date}}]]), the time grouping ({{time_perspective}} :: DATE AS periodon Postgres/Redshift;DATE({{time_perspective}})on SQLite). Cards wrap it:WITH base AS ({{snippet: name}}) SELECT …. Never put{{…}}in SQL comments (Metabase expands them).filters.yaml— the template tags the snippets use: field filters (type: dimension,dimension: [field, {base-type: type/Date}, <field id>],alias: table_alias.column,widget-type) and thetemporal-unittag. Shared tags live here once; the example is inspecs/project-format.md.cards/<key>.sql+cards/<key>.yaml— one per card. SQL followsspecs/sql-style-guide.md; the .yaml has name, description, display and chart settings. Chart cards always setgraph.dimensions,graph.metrics(andgraph.x_axis.scale: timeseriesfor dates) — see the table at the top ofvisualization-cookbook.md.dashboard.yaml— filters (parameters), tabs, placements withat: [col, row, width, height], per-tilevizoverrides, and filter wiringfilters: {p_date: date}on every card that has the tag.
SQL vs MBQL: prefer native SQL for anything reviewed, versioned or reused (CTEs, window functions, custom date
logic, snippets); MBQL for simple aggregations and reusable metrics. Structured questions work in projects too
(their query: is kept as-is).
Parameterized SQL — two different mechanisms:
- Basic variables (
text,number,date): plain substitution, you write the operator —WHERE status = {{status}}, optional with[[AND category = {{category}}]]. - Field filters (
dimension): Metabase writes the condition —WHERE {{date_range}} [[AND {{category}}]]. NeverWHERE col = {{tag}}(the pre-flight rejects it). With table aliases,aliasmust bealias.column.
Phase 3: Check, publish, look
project validate <dir> # must be clean
project lint <dir> # fix, or record deliberate choices under lint.ignore in project.yaml
project test <dir> # every card runs with the default filters, and chart settings are checked
# against the columns each query really returns — must be all ok
project diff <dir> # show the user what will be created
project push <dir> # after the user approves
project lint <dir> # again: column-based rules (number formats, …) only run once cards exist in Metabase
Then run the visual check (below) — mandatory. Metabase may draw a sensible chart even when a setting is wrong
(it falls back to another column), so a good-looking picture doesn't replace a clean project test.
Phase 4: Test and hand off
project test <dir> --images— every card ran; PNG per card and per tab in<dir>/.mbsync/render/.- Spot-check 2–3 key numbers against known values or a direct SQL run.
- Filters: render with other values (
dashboard render <id> --params '{"category": "Gizmo"}') and confirm cards change. - Edge cases: empty results, nulls, an empty filter.
- Hand off with
project render <dir> --pdfso the user sees the final result without opening Metabase, and commit the project folder.
The Visual Check (after any card or layout change)
Metabase will happily save a chart that is wrong. Look at it:
- After creating or changing a card: for a card on a dashboard,
dashcard image <dashboard-id> <tile title>(what viewers see: the tile's own title, displayed value and formatting, with the dashboard's default filters — a card placed three times needs three checks); for a standalone question,card image <id>. Read the PNG. Check:- the chart type fits the question; axes use the intended columns
- labels readable and not overlapping; sensible sort order
- number formats (currency, %, decimals — no
44,383.9); colours consistent with the dashboard - not empty and no error (the JSON's
errorfield); title and description present
- Fix and re-render — at most 3 rounds per card. After that, show the user the image and ask.
- After any layout change:
dashboard render <id>(orproject render <dir>) → Read each tab's PNG. Check:- rows fill the 24 columns; KPI tiles align and read at a glance
- headings separate sections; charts aren't cramped
- the filter bar has every filter; reading order makes sense
card_errorsin the JSON is empty for every tab
- At hand-off, attach a
--pdfrender.
Rendering shows what Metabase has now — push project edits first.
STUDY Workflow
Run this before claiming you understand a dashboard. JSON-only reading routinely misdescribes dashboards whose cards are placed several times with different settings.
- Summary —
dashboard <id>:collection_authority_level(official= curated),card_countvsunique_card_count,repeated_card_ids, per-placementoverrides(title, scalar_field, graph_metrics, …). - Look at it —
dashboard render <id>→ Read every tab's PNG. This replaces asking for a screenshot; ask only if rendering fails.card_errorslists broken cards. - Each unique card —
card <id>(result columns, snippets, template tags);--full --outonly for the SQL. - Each repeated placement —
dashcard <dashboard-id> <index|name>for the full settings. - Shared snippets —
snippet <id>for every referenced snippet. - Produced vs displayed columns — compare
result_columnswith each placement'sscalar_field/graph_metrics. - Parameters — type, default, and which tags/fields each maps to.
- Data shape —
card query <id> [--out <file>]on the most complex card.
(If the dashboard is a project folder, project pull then reading dashboard.yaml covers steps 1, 4 and 7.)
EDIT Workflow
Dashboards kept as a project folder
project pull <dir> # bring in anything changed in Metabase (refuses to overwrite local edits)
# edit the YAML / SQL
project validate <dir> && project lint <dir>
project test <dir> --card <key> # the cards you touched
project diff <dir> # show the user
project push <dir> # then the visual check
- The files are the source of truth. Someone changed it in the Metabase UI?
project pulland commit (ids.json too: it records when each item was synced, and that is what the drift check compares against). - If push reports drift ("changed in Metabase since these files were last synced"), stop and ask: keep the Metabase change (
pull --forceafter saving your edits elsewhere) or overwrite it (push --yes). - To stop managing a card without archiving it, remove it from
cards/and fromids.json.
Dashboards not kept as files
- STUDY first — it catches per-placement overrides you'd otherwise overwrite.
- The live card is the source of truth, not any repo
.sqlfile: start fromcard <id> --full --out. - Layout:
dashcard update <dash> <index|name> --patch f.jsonfor one placement; for bigger changesdashboard <id> --layout --out f.json, edit,dashboard put <id> --from f.json. - Card query/settings:
card update <id> --patch f.json(checked before saving). - The visual check.
- Offer to turn it into a project:
project pull <dir> --dashboard <id>(no-op in Metabase; gives files, diff, history in git).
Don't read a repo's old build/deploy scripts to "understand how things fit together" — the CLI is sufficient. Read them only for a convention invisible in the live payload (e.g. a field-id registry) or when asked to change them.
SHARE & SCHEDULE
- A picture or PDF for someone:
dashboard render <id> --server --out <dir>/name.pdf(Metabase's own print layout, A4/letter, active filters listed — the best choice for sharing),dashboard render <id> --out name.pdf(screenshots of the live UI, one page per tab) orcard image <id> --out name.png. Filter values:--params(dashboard: filter slugs; card: tag names). - Alert (one card, on a condition): write the body per
specs/notifications-api-spec.md, confirm recipients, thenalert create --from f.json.goal_above/belowneeds a goal line on the card; otherwise shape the card sohas_resultexpresses the condition. Rehearse with yourself as the only recipient beforealert send. - Subscription (a dashboard on a schedule):
subscription create --from f.json; leavecardsout to include every card. In a project, put it undersubscriptions:indashboard.yamland push. - Both warn when email/Slack isn't configured — tell the user an admin must set it up; don't create deliveries that silently go nowhere. Report schedules in the instance's report timezone (printed on create).
HISTORY
card history <id> / dashboard history <id> list revisions (who, when, what). Confirm the target with the user, then
card revert <id> --to <revision-id>. For a project folder, project pull afterwards.
EXPLORE Workflow
databases→tables --database <id>→collections --tree(★ = official) →collection-items <id>→search <query>→card <id>/dashboard <id>.- Official collections (
authority_level: "official") show a medal badge; promoting one is Pro/Enterprise.
Shortcut: when the intent is clear ("show me Q3 sales by region"), skip discovery and go to SQL.
USAGE-ANALYTICS Workflow (Pro/Enterprise)
Adoption, stale content, query performance, who uses what. Refused early on the free edition.
usage-analytics— models and their card ids (ids vary per instance; never hardcode).- Prefer the 8 built-in analytics dashboards when one answers the question.
- Otherwise MBQL (native SQL is blocked on the audit database): write the query to a file,
usage-analytics query --from <file>; browsing:usage-analytics query --card <id> --limit N.
Rules: always date-filter View/Query/Activity logs (7–30 days); query_source separates human (dashboard, ad-hoc,
question, collection) from automated (pulse, dashboard-subscription, cache-refresh); use the Content model's
question_is_native for SQL-vs-MBQL counts; "External User" = embedded views.
Defensive Guardrails
- Every SQL query must aggregate or LIMIT. No unbounded
SELECT *on large tables. Queries over 2 minutes suggest missing date filters on sort keys, pre-aggregation or materialized views. - Test SQL before publishing (
project test,card query, or a database skill the project uses). - Delete means archive.
card delete/dashboard deletearchive (restore withrestore);--permanent --yesonly when the user asks for a hard delete. Collections and snippets can only be archived. - Pre-flight checks exist for a reason. Fix what they report; use
--skip-validateonly when you know the check is wrong, and say so. - Deliveries reach real people. Confirm recipients before creating alerts/subscriptions or running
alert send.
Common Gotchas
Metabase v63 payloads (verified on v0.63.18):
- Template tags are returned as an array (
[{name: …}]) even when written as a map; read either shape. - Stored field refs carry random
lib/uuidstamps, and structured queries get freshlib/uuids on every read — not changes. (Project diff/pull already ignore them.) - A native card's
parametersstay[]; set them only for static-list dropdowns. - The Sample Database is SQLite on v63 (H2 before): no
DATE_TRUNC—strftime('%Y-%m-01', col); check the engine withdatabases. Metabase types such a month column as a number; keep it ingraph.dimensionswithgraph.x_axis.scale: timeseries.
Projects (config as code):
- Commit
ids.jsonwith every pull and push: itsupdated_atvalues are what protects a teammate's push from overwriting changes made in the Metabase UI. A folder with no recorded timestamps makes push warn on every item. - One owner per snippet. If cards in two projects use the same snippet, both pulls write it and the last push
wins. Keep it in one project and list it under
external_snippetsin the others. --loose-cardson one project per collection, never on two.- Never pull personal collections into a shared repo.
- Prefer native SQL cards with snippets for anything edited as files; structured (MBQL) questions round-trip exactly but are awkward to edit by hand.
Dashboard PUT and layout:
tabsmust accompanydashcards; every dashcard needscard_id(nullfor virtual cards); negative ids create new tabs/placements; omitted placements are deleted.- Placement ids change when a layout is re-created — subscriptions point at them.
project pushreuses existing ids; hand-written layouts should too (GET fresh state first). - The 24-column grid: full width is
size_x: 24; keepcol + size_x ≤ 24; Metabase stores overlaps as sent. - Placement
visualization_settingsoverride the card's for that placement only;column_settingson a placement replace the card's wholecolumn_settings.
Dashboard summary interpretation:
card_countcounts placements;unique_card_countdistinct cards;repeated_card_idsshows cards placed several times — almost always with different titles/metrics. Rundashcardon each before describing the dashboard.overridesis a curated subset (title, scalar_field, graph_metrics, graph_dimensions, series_titles). For combined visualizations the settings sit undervisualization.settingswithCOLUMN_Nnames; the summary resolves them.
Filter wiring:
- A filter only works where it's mapped. Native SQL cards need the template tag defined before a mapping works.
- Targets: field filters / time grouping
["dimension", ["template-tag", t], {"stage-number": 0}]; plain variables exactly["variable", ["template-tag", t]](a third item is rejected); text cards["text-tag", t]. - Field refs in mappings need
{"base-type": "type/Text"}-style options, notnull. - Tags used only inside a snippet must still be declared on every card using the snippet (projects do this for you), and nested snippets must all be declared as snippet tags on the card.
- Field filters can't target derived columns (CASE, computed buckets) — use a basic
textvariable with a static-list dashboard filter. - Relative date defaults:
past7daysexcludes today;past7days~includes it. - A static-list
string/=filter needs"values_query_type": "list"to render as a dropdown. - Text cards can show filter values:
{{d}}in the text + a mapping with target["text-tag", "d"],card_id: null.
Display types:
pivotis structured-query only; native SQL →display: "table"+"table.pivot": true,table.pivot_column,table.cell_column. Build pivots withpivot build(never hand-write the MBQL);card queryon a pivot card returns the grid users see and exports are pivoted by default (--flatfor rows). Details: cookbook "Pivot Table".- Never create a chart card with
visualization_settings: {}— setgraph.dimensions,graph.metrics, andgraph.x_axis.scale: timeseriesfor dates. Scalars and tables can start empty. - Headings: plain text; text cards: markdown. Heading recipe:
{"dashcard.background": false, "virtual_card": {"name": null, "display": "heading", "visualization_settings": {}, "archived": false}, "text": "Section"}.
SQL:
- pMBQL only for native cards:
{"lib/type": "mbql/query", "database": <id>, "stages": [{"lib/type": "mbql.stage/native", "native": "<SQL>", "template-tags": {...}}]}— the legacy format silently drops the database. - Read SQL from
dataset_query.stages[0].native, tags fromstages[0].template-tags. - Column aliases are human-readable and quoted:
AS "Revenue". Followspecs/sql-style-guide.mdfrom the start. - Never write
{{snippet: x}}inside a SQL comment — Metabase expands it there too.
Sign-in and editions:
- Browser sign-in needs v63+ and the app-registration switch that rides on Admin > AI > MCP.
- Chrome rendering needs Chrome and Node 22+;
--serverPDFs need neither. Both show what Metabase has now. There is no server-side PNG for single cards on v63 (the download menu's PNG is drawn in the browser). - Pro/Enterprise-only commands are refused on the free edition with a clear message (
--skip-preflightto try).
CLI: --patch / --from take file paths only. Batch updates: write all patch files first, then loop.
Example Prompts
"Build me a store performance dashboard: weekly installs, conversion and ratings as KPIs, a trend line, a breakdown by country, with date and country filters."
NEW → design (metrics/dimensions/filters → text → HTML mockup) → project new → snippets, filters.yaml, cards, dashboard.yaml
→ validate / lint / test → diff → push → visual check (card images, dashboard render) → test --images → PDF hand-off.
"Show me what dashboard 134 looks like — is anything broken?"
STUDY step 2: dashboard render 134 → Read each tab → report layout and card_errors.
"The date filter on dashboard 22 doesn't affect the Revenue by State card."
Project folder? project pull, check that placement's filters: and the card's tags, fix, diff, push, render.
Otherwise dashcard 22 "Revenue by State", add the tag to the card if missing (card update), wire it with
dashcard update, then dashboard render 22 --params '{"date": "past30days~"}' to confirm.
"Send me a PDF of the store dashboard." —
dashboard render 134 --out <dir>/store-analytics.pdf.
"Email the exec dashboard to the leadership list every Monday at 8." — confirm addresses → subscription (weekly, mon, 8) → report the timezone. In a project: add it under
subscriptions:and push.
"Alert me when yesterday's signups drop below 50." — shape a card that returns a row only when signups < 50 → alert with
has_result, daily cron.
"Who changed card 402 last week? Put it back." —
card history 402→ confirm the revision →card revert.
"Tidy up the Commercial collection." — REORGANIZE:
collections --tree,collection-items, propose a structure, thencollection createand move items viacard update/dashboard updatewithcollection_id.
"Am I signed in? Which version is it?" —
status.