| name | core |
| description | Foundations for driving Metabase from the terminal with the `mb` CLI — authentication and named profiles, the flag/output/`--json` conventions every command shares, JSON body input, command discovery via `--help` (add `--json` for machine-readable schemas), and the per-resource footguns (db, table, field, upload, card, dashboard, collection, segment, measure, timeline, alert, subscription, library, setting, search, eid). Load first for any `mb` task; it routes to the specialized skills for deeper work. |
| allowed-tools | Read, Write, Edit, Bash, AskUserQuestion |
metabase-cli (core)
The official Metabase CLI (mb) drives a Metabase instance over its REST API: auth, list/get/create/update/delete on every resource, query and transform execution, content search, git-sync (representations ↔ instance), and entity-id translation.
Top-level command groups (run mb <group> --help to discover verbs):
auth | db | table | field | upload | query | card | dashboard | snippet | segment | measure | collection | library
document | timeline | timeline-event | transform | transform-job | transform-tag | alert | subscription | setting
search | git-sync | setup | eid | uuid | upgrade | skills
The conventions below — auth, flags, output, body input — hold across every group. Per-command flags and examples live in each command's --help; add --json for the machine-readable form with the output JSON Schema. A few flows have their own skills (see "Specialized skills"). When a card needs a query, prefer MBQL over native SQL (portable, pre-flight-validated — load mbql); fall back to native SQL when MBQL can't express it.
Auth & profiles
The agent does not log in for the user. Authentication is the human's job — they pick the base URL, paste credentials, and store them as a named profile. The agent checks what profiles exist, asks which to use, and passes --profile <name> through every command.
mb auth list --json
mb auth status --json
mb auth status --profile <name> --json
auth list is the primary enumeration path — one call returns every profile with sanitized URL, an authenticated flag, and a probe status (ok / auth-failed / network-error / server-error / not-probed). Use it before asking which profile to pick.
- One profile and intent doesn't disambiguate → use it.
- Several → ask via
AskUserQuestion, presenting the names from auth list.
- Empty
data: [] → ask the user to run mb auth login themselves and tell you the profile name.
Once a name is established, pass --profile <name> to every subsequent command. Profile names are arbitrary local labels (prod, staging).
Flag conventions
--profile is per-subcommand — it attaches after the full verb chain, not before it.
✅ mb table list --profile prod --json
❌ mb --profile prod table list
--wait for async operations. transform run, git-sync import, and similar verbs return immediately by default. Pass --wait whenever the next step depends on completion — without it you race the operation and see "not ready" / transient connection refusals.
Some "lookup" verbs return JSON envelopes, not bare values. mb setting get <key> returns {"key": "...", "value": ...}. Extract before reusing:
VALUE=$(mb setting get <key> --json | jq -r '.value')
Output
Every list/get verb supports the same output flags:
--json — emit the full JSON envelope, safe for jq. Default is human-readable text.
--full — include every field (the compact projection is the default, and is the agent-facing contract).
--fields a,b.c.d — project specific dot-paths. Mutually exclusive with --full. Paths are relative to each data[] item on list verbs, and to the root on single-item verbs. So it's --fields id,name on … list / database schema-tables (data.id and data[].id both fail with unknown field path: "data.id"), and --fields id,name,display on card get, --fields data.rows on mb query (whose data is an object).
--max-bytes <n> — cap output size. Default 24576 (sized to fit under agent-harness tool-output limits); 0 disables. On a list it drops trailing items and sets truncated (see below). Single-item commands (get) never truncate — over the cap they throw a ConfigError (exit 2: "output is N bytes, over the M-byte --max-bytes cap; …") whose tail names the remedy: on schema-shaped commands it is the exact narrower command to run instead — follow it rather than raising the cap.
- JSON output is a single line when stdout is piped (pretty-printed only at a TTY) — always parse it, never scrape by line position.
List windows and resumption
Every list verb takes --limit <n> (items this call returns) and --offset <n> (where the window starts, default 0), and answers with one envelope, metadata first so the counts and the resumption point survive a cut tail: {returned, offset, limit?, total, has_more, next_offset, truncated?, data}. truncated is {reason: "max_bytes", bytes: N}.
has_more decides whether to keep going — never compare counts. total is the server's count on endpoints that report one and null on those that don't, so arithmetic over it is not a termination condition.
- To continue, pass
next_offset back as --offset. When has_more is true next_offset is past the offset you sent, so the loop advances; when false the walk is over and next_offset is null.
truncated means the byte cap cut the output, not that the data ran out. has_more/next_offset are recomputed to the cut point, so a capped list resumes like any window. Its bytes is what the untruncated answer would have measured, so it sizes the work left rather than the reply you hold. Narrow rows with --fields rather than raising --max-bytes — a bigger cap spends context on fields you didn't ask for, and the cap counts only what you asked for, so --fields buys rows directly. A capped list always returns at least one row; when not even one fits it exits 2 with "the smallest response this list can produce is N bytes, over the M-byte --max-bytes cap; …".
limit is echoed only when you passed --limit — except mb search, which defaults to --limit 20 (an unbounded search is expensive server-side) and so always reports one. On nouns the server doesn't page, one large --limit with narrow --fields is a single request; many small --offset hops are one request each.
The whole walk, literally:
offset=0
while : ; do
out=$(mb table list --db-id 1 --limit 50 --offset "$offset" --fields id,name --profile <n> --json)
echo "$out" | jq -c '.data[]'
[ "$(echo "$out" | jq -r '.has_more')" = "true" ] || break
offset=$(echo "$out" | jq -r '.next_offset')
done
Body input (create / update / run)
Verbs that take a payload accept it from one of four sources, first non-empty wins:
--body '<inline JSON>'
--file <path> — JSON file
- stdin (auto-detected when piped;
--file - names it explicitly)
- positional argument
Exactly one required; passing more than one of --body / --file / a positional argument is rejected with a ConfigError.
cat > ./.scratch/body.json <<'EOF'
{ ... }
EOF
mb <noun> create --file ./.scratch/body.json --profile <n> --json
Single-quoted 'EOF' stops the shell interpolating $vars inside the JSON.
Write working files to ./.scratch in the current directory (mkdir -p ./.scratch first), never /tmp — better permissions, they persist across the session, and the user can review them.
Discovering commands and schemas
Cheapest source that answers the question wins:
- What groups/verbs exist? →
mb --help, then mb <group> --help. Add --json for a machine-readable {command, description} index (mb --help --json lists every command).
- What flags does a command take? →
mb <command> --help — flags with enums and defaults, examples, ~1 KB.
- Output JSON Schema before parsing, JSON-body input schema before authoring, machine-readable arg types, min server version? →
mb <command> --help --json — that command's full entry (inputSchema is the exact validator the command runs on the body; null when it takes none).
mb card query --help
mb card list --help --json | jq .outputSchema
mb card create --help --json | jq .inputSchema
mb transform --help --json | jq -r '.commands[].command'
Resource quirks worth memorizing
Routine verb shapes (list / get / create / update), every flag, and output schemas live in each command's --help (add --json for output schemas). Below is only what help does not tell you: footguns and non-obvious behaviors.
- db traversal: the hydration ladder. Start with
database get <db-id> --include tables — the compact table map (id, name, schema, description per table), one call that fits most databases. Pick the relevant tables, then table fields <table-id> per table (bounded: fields are per-table). --include tables.fields is the full rollup — small databases only. Hundreds of tables? Traverse by schema (database schemas <db-id> → database schema-tables <db-id> <schema>) or look tables up by name (search <term> --models table --db-id <db-id> --limit 10). sync-schema / rescan-values queue async work and return {status:"ok"} immediately; sync-schema --wait blocks until initial_sync_status: complete.
- table fields.
table get never returns fields on its own — pass --include fields (compact; the underlying query_metadata response also carries FK targets and dimensions, visible under --full) or use table fields <id> (list envelope). table update patches table-level metadata only; physical columns aren't editable.
- field has no
list. Fields are per-table — get them via table get <id> --include fields. Never enumerate fields across a whole db (context blow-up). field summary is live cardinality {field_id, count, distincts}; field values is the cached distinct set (has_more_values: true ⇒ truncated cache). field update patches metadata only (base_type isn't editable) — this is where you set a column's semantic_type or foreign-key target.
- upload (CSV → tables).
upload csv --file <path> creates a new table + model (prints {model_id, table_id}); upload append <table-id> / upload replace <table-id> --file <path> add to / overwrite a table previously created by upload (columns must match). The destination db+schema is admin-configured, not per-call — check with mb setting get uploads-settings --json (db_id: null ⇒ uploads off/unconfigured; needs admin to read). --collection <id|root> only sets the model's collection. Max 50 MB. Errors: = no db has uploads enabled; = the append/replace target isn't an uploaded table.
Specialized skills (load on demand)
This file is enough for any single-command task. For anything deeper, load the relevant skill proactively — don't wing an MBQL body, a transform body, or the git-sync workflow from this overview. Load via mb skills get <name>.
mbql — authoring/fixing any MBQL query body (mb query, card dataset_query, transform source.query, measure/segment definition); reading --dry-run errors. The query-body reference.
native-sql — authoring a native SQL dataset_query with parameters: template tags, field filters vs. raw variables, snippets, card references, and wiring a tag to a dashboard filter. The SQL fallback when MBQL can't express it (mbql first).
visualization — choosing a card's display and authoring visualization_settings. The presentation counterpart to mbql.
dashboard — building interactive dashboards: wiring filters (parameters + mappings), linked/cascading filters, cross-filtering, click behavior, series, and tabs. Load beyond a plain card-layout task.
metadata — setting field/table metadata: semantic types, foreign-key targets, dropdown/scan behavior, and column visibility, and the downstream features each unlocks. Load when editing what a column means, not its data.
notification — scheduled delivery: question alerts (mb alert) and dashboard subscriptions (mb subscription). Choosing between them, the two schedule/recipient contracts, channel prerequisites, testing a send.
transform — transform body JSON, create + run-with-wait, run inspection, tags, jobs.
document — Metabase documents (TipTap body, embedding cards).
git-sync — round-tripping content to/from a git remote.
data-workflow — the guided, end-to-end data workflow: investigate raw data, build clean analysis-ready tables, define reusable segments/measures/metrics, answer questions, build dashboards. Start here when the user states a goal rather than a single verb — "make sense of my data", "build a data model", "go from raw data to a dashboard", "be my data analyst", "set up analytics for X". It detects where the data is and routes to the right stage.
If a task spans more than one, load each. mb skills list enumerates everything on the installed version.
Don't
- Don't paste credentials or warehouse passwords in chat. Have the user run the storing command.
- Don't shell into
curl against /api/... (or add an HTTP library) when a mb <verb> exists — that bypasses retries, schema validation, and credential redaction.