{"id":51905215,"url":"https://github.com/metabase/metabase-cli","last_synced_at":"2026-07-26T21:01:00.975Z","repository":{"id":367648457,"uuid":"1229138009","full_name":"metabase/metabase-cli","owner":"metabase","description":"`mb` is a command-line client for Metabase","archived":false,"fork":false,"pushed_at":"2026-07-08T00:43:29.000Z","size":1377,"stargazers_count":3,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-08T02:11:08.644Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://www.metabase.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/metabase.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-04T18:32:54.000Z","updated_at":"2026-07-08T00:43:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/metabase/metabase-cli","commit_stats":null,"previous_names":["metabase/metabase-cli","metabase/mb-cli"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/metabase/metabase-cli","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metabase%2Fmetabase-cli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metabase%2Fmetabase-cli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metabase%2Fmetabase-cli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metabase%2Fmetabase-cli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/metabase","download_url":"https://codeload.github.com/metabase/metabase-cli/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metabase%2Fmetabase-cli/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35928365,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-26T02:00:06.503Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-26T21:01:00.021Z","updated_at":"2026-07-26T21:01:00.899Z","avatar_url":"https://github.com/metabase.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# metabase-cli\n\nCommand-line client for Metabase. Logs in to an instance in your browser (OAuth, Metabase v62+) or with an API key, and stores credentials securely on your machine.\n\n## Supported Metabase versions\n\nThe minimum supported server is **Metabase v0.58** (major `58`). Anything older is unsupported.\n\nCommands that need more than a baseline OSS server declare it — a higher minimum major version or a premium token feature. The server version and token features are detected and cached when you run `mb auth login` (or `mb auth list`). For those commands, a preflight check runs before the first request and refuses with an actionable message (exit code `2`) when:\n\n- the server is older than the command's minimum version, or\n- the command needs a premium feature (e.g. `remote_sync`, `transforms`) that isn't enabled.\n\nPlain OSS commands against a v0.58+ server (the majority) carry no elevated requirement and skip the preflight entirely. When a gated command runs but the server version can't be detected (no cached probe), it proceeds with a warning rather than refusing. To bypass the check for a single run, pass `--skip-preflight`; to bypass it process-wide (e.g. in CI), set `MB_CLI_SKIP_PREFLIGHT=1`. Both are footguns — only for servers you know are patched.\n\n## Install\n\n```sh\nnpm install -g @metabase/cli\nmb --help\n```\n\nOr build from source:\n\n```sh\nbun install\nbun run build\nnode dist/cli.mjs --help\n```\n\nThe binary is `mb`. Examples below use that name.\n\n## Quick start\n\n```sh\nmb auth login --url https://metabase.example.com\nmb auth status\n```\n\n## Authentication\n\nCredentials are stored per-profile. The default profile is named `default`. Use `--profile \u003cname\u003e` to manage additional profiles.\n\n### `mb auth login`\n\nLog in to a Metabase instance and save the credential to a profile. Interactive login offers two methods:\n\n- **In your browser** (recommended; requires Metabase v62 or newer) — the CLI opens Metabase, you sign in with your password or SSO and approve the CLI, and a short-lived access token plus a rotating refresh token are stored. Tokens refresh automatically; you never paste a secret.\n- **With an API key** — paste a key from Admin settings → Authentication → API keys.\n\nAgainst a server older than v62 the CLI detects the missing OAuth support and falls back to the API key prompt automatically. Supplying an API key (flag, env, or stdin) always skips the browser flow, so CI and scripts behave exactly as before.\n\nOn success the server is probed once — the rendered output shows the user, role (`Admin`/`User`), and Metabase version, and the same values are cached in `\u003cconfigDir\u003e/profiles.json` so later commands skip re-probing. Failure of either the auth probe (`/api/user/current`) or the server probe (`/api/session/properties`) rejects the login; an existing profile keeps its last-known-good credential and gains a `lastFailure` entry.\n\n| Flag                     | Description                                                                                                                                    |\n| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |\n| `--url \u003curl\u003e`            | Metabase URL, including any subpath if the instance is hosted under one (`https://my.org.com/metabase`). Falls back to `MB_URL`, then prompts. |\n| `--api-key \u003cvalue\u003e`      | API key. Skips the browser flow. Visible in shell history — pipe on stdin instead.                                                             |\n| `--client-id \u003cid\u003e`       | Pre-registered OAuth client id (only needed when dynamic client registration is disabled on the server).                                       |\n| `--profile \u003cname\u003e`, `-p` | Profile to write to (default: `default`).                                                                                                      |\n| `--skip-verify`          | Save without contacting the server (no probe, no cache).                                                                                       |\n\nNon-interactive (non-TTY) login requires an API key; resolution order: `--api-key` → piped stdin → `MB_API_KEY` (first non-empty wins). Without one, non-interactive login fails rather than prompting.\n\n```sh\nmb auth login                                            # interactive: browser or API key\necho \"$MB_KEY\" | mb auth login --url https://m.example.com\nmb auth login --url https://m.example.com \u003c key.txt\n```\n\n### `mb auth status`\n\nShow whether a profile is authenticated. The output includes the auth method (`OAuth` or `API key`) alongside the cached user, role, and server version.\n\n```sh\nmb auth status\nmb auth status --json\nmb auth status --profile staging\n```\n\n| Flag                     | Description                              |\n| ------------------------ | ---------------------------------------- |\n| `--profile \u003cname\u003e`, `-p` | Profile to inspect (default: `default`). |\n| `--json`                 | Emit JSON. Auto-enabled on non-TTY.      |\n\n### `mb auth list`\n\nList configured authentication profiles. All profile metadata (URL, auth method, last successful probe, last failure) lives in `\u003cconfigDir\u003e/profiles.json` at mode `0600`; the secrets (API key, or OAuth access/refresh tokens) sit in the OS keychain when available, or inline in the same file when the keychain is unavailable.\n\n`auth list` re-probes every profile, one at a time — a probe can refresh and rewrite an expired OAuth token, so probes are serialized to avoid racing on the shared `profiles.json`. On success it refreshes `lastProbe` (Metabase version, token features, user identity) and clears `lastFailure`; on failure it updates `lastFailure` and leaves the prior `lastProbe`/`url`/credential untouched. Rendered columns: `Profile | URL | Auth | Status | Role | Version | Last probed`. Failed rows append a one-line footer pointing at `mb auth login --profile \u003cname\u003e`.\n\n```sh\nmb auth list\nmb auth list --json\n```\n\n| Flag     | Description                         |\n| -------- | ----------------------------------- |\n| `--json` | Emit JSON. Auto-enabled on non-TTY. |\n\n### `mb auth logout`\n\nClear stored credentials for a profile. For an OAuth profile the refresh token is also revoked server-side, best-effort: local credentials are cleared first and a revocation failure only warns, so a slow or offline server never blocks the logout.\n\n```sh\nmb auth logout --yes\nmb auth logout --profile staging --yes\n```\n\n| Flag                     | Description                                                                                                                       |\n| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |\n| `--profile \u003cname\u003e`, `-p` | Profile to clear (default: `default`).                                                                                            |\n| `--yes`                  | Skip the interactive confirmation prompt. In non-TTY contexts the prompt is skipped automatically (kubectl/gh/docker convention). |\n\n## Transforms\n\nCRUD on `/api/transform`. Bodies for `create` / `update` are JSON; resolution order: `--body` → `--file` → piped stdin (auto-detected when stdin is not a TTY).\n\n### `mb transform list`\n\n```sh\nmb transform list\nmb transform list --json\n```\n\n### `mb transform get \u003cid\u003e`\n\n```sh\nmb transform get 1 --json\n```\n\n### `mb transform dependencies \u003cid\u003e`\n\nList the upstream transforms this transform depends on (the ones that must run before it). The positional id is a transform id.\n\n```sh\nmb transform dependencies 1 --json\n```\n\n### `mb transform create`\n\n```sh\ncat transform.json | mb transform create\nmb transform create --file transform.json\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\n### `mb transform update \u003cid\u003e`\n\n```sh\nmb transform update 1 --body '{\"name\":\"renamed\"}'\n```\n\nSame `--body` / `--file` resolution as `create`. Stdin is auto-detected when not a TTY.\n\n### `mb transform delete \u003cid\u003e`\n\n```sh\nmb transform delete 1 --yes\n```\n\n| Flag    | Description                                                                                                                       |\n| ------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| `--yes` | Skip the interactive confirmation prompt. In non-TTY contexts the prompt is skipped automatically (kubectl/gh/docker convention). |\n\n### `mb transform run \u003cid\u003e`\n\nTrigger a manual run. Returns `{message, run_id}` and exits immediately. Pass `--wait` to poll until the run reaches a terminal status (`succeeded`, `failed`, `timeout`, `canceled`); the `final` field on the result holds the polled run state, and the command exits 1 if the final status is anything but `succeeded`. Pass `--sync` to additionally wait until the run's output table is registered and surface its `target_table_id`, so you can build MBQL cards against it — the run registers the table itself, so no separate `db sync-schema` is needed; `--sync` implies `--wait`.\n\n```sh\nmb transform run 1\nmb transform run 1 --wait --json\nmb transform run 1 --sync --json\n```\n\n| Flag              | Description                                                                                                            |\n| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `--wait`          | Poll until the run reaches a terminal status.                                                                          |\n| `--sync`          | After a successful run, wait until the output table is registered and return its `target_table_id` (implies `--wait`). |\n| `--timeout \u003cms\u003e`  | Polling timeout in ms (default 600000). Used with `--wait`.                                                            |\n| `--interval \u003cms\u003e` | Polling interval in ms (default 2000). Used with `--wait`.                                                             |\n\n### `mb transform cancel \u003cid\u003e`\n\nCancel the currently-running run for a transform. Exits 0 with `{canceled: true, id}` on success; exits 1 with a 404 if the transform has no active run.\n\n```sh\nmb transform cancel 1\nmb transform cancel 1 --json\n```\n\n### `mb transform get-run \u003crun-id\u003e`\n\nFetch a single run by run id (not transform id). Same compact / `--full` projection convention as `transform get`.\n\n```sh\nmb transform get-run 1 --json\n```\n\n### `mb transform runs`\n\nList recent transform runs across all transforms, or filter to one. Drains all pages by default; pass `--limit` to cap.\n\n```sh\nmb transform runs\nmb transform runs --transform-id 1 --json\nmb transform runs --limit 10 --json\n```\n\n| Flag                  | Description                                         |\n| --------------------- | --------------------------------------------------- |\n| `--transform-id \u003cid\u003e` | Filter to runs of a single transform id.            |\n| `--limit \u003cn\u003e`         | Cap total runs returned (default: drain all pages). |\n\n## Transform jobs\n\nCRUD on `/api/transform-job`. Bodies for `create` / `update` follow the same `--body` / `--file` / stdin pattern as transforms.\n\n### `mb transform-job list`\n\n```sh\nmb transform-job list --json\n```\n\n### `mb transform-job get \u003cid\u003e`\n\n```sh\nmb transform-job get 1 --json\n```\n\n### `mb transform-job create`\n\n```sh\nmb transform-job create --body '{\"name\":\"daily\",\"schedule\":\"0 0 0 * * ?\"}'\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\n### `mb transform-job update \u003cid\u003e`\n\n```sh\nmb transform-job update 1 --body '{\"schedule\":\"0 0 6 * * ?\"}'\n```\n\n### `mb transform-job delete \u003cid\u003e`\n\n```sh\nmb transform-job delete 1 --yes\n```\n\n| Flag    | Description                                                                                                                       |\n| ------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| `--yes` | Skip the interactive confirmation prompt. In non-TTY contexts the prompt is skipped automatically (kubectl/gh/docker convention). |\n\n### `mb transform-job run \u003cid\u003e`\n\nTrigger a job manually and return immediately. The job runs every transform carrying one of the job's tags, plus those transforms' dependencies.\n\n```sh\nmb transform-job run 1\nmb transform-job run 1 --force-refresh --json\n```\n\n| Flag              | Description                                                                                         |\n| ----------------- | --------------------------------------------------------------------------------------------------- |\n| `--force-refresh` | Re-run the whole plan, including dependency transforms that are already fresh (skipped by default). |\n\n### `mb transform-job transforms \u003cid\u003e`\n\nList the transforms a job would run, resolved by the job's tags. The positional id is a job id.\n\n```sh\nmb transform-job transforms 1 --json\n```\n\n### `mb transform-job set-active \u003ctrue|false\u003e`\n\nActivate or deactivate every transform job at once (admin only). Inactive jobs do not run on schedule; manual runs ignore the flag.\n\n```sh\nmb transform-job set-active false\nmb transform-job set-active true --json\n```\n\n## Transform tags\n\nCRUD on `/api/transform-tag`. Tags group transforms and jobs; reference them by id via the `tag_ids` field on a transform or job. The four built-in tags (`hourly`, `daily`, `weekly`, `monthly`) drive the built-in jobs. There is no get-by-id endpoint — use `list`.\n\n### `mb transform-tag list`\n\n```sh\nmb transform-tag list --json\n```\n\n### `mb transform-tag create`\n\n```sh\nmb transform-tag create --body '{\"name\":\"nightly\"}'\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\n### `mb transform-tag update \u003cid\u003e`\n\n```sh\nmb transform-tag update 5 --body '{\"name\":\"renamed\"}'\n```\n\n### `mb transform-tag delete \u003cid\u003e`\n\n```sh\nmb transform-tag delete 5 --yes\n```\n\n| Flag    | Description                                                                                                                       |\n| ------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| `--yes` | Skip the interactive confirmation prompt. In non-TTY contexts the prompt is skipped automatically (kubectl/gh/docker convention). |\n\n## Databases\n\nRead warehouse metadata from `/api/database`. The `db` group exposes the full database list, the per-database record with optional table/field hydration, schema and table inspection, and the two manual-sync triggers.\n\n`db` is aliased to `database`.\n\n\u003e **Agent traversal — the hydration ladder:** start with `db get \u003cdb-id\u003e --include tables`, the compact table map (id, name, schema, description per table) — one call that fits most databases. Pick the relevant tables, then fetch fields per table with `table fields \u003ctable-id\u003e` (bounded: a table has at most a few hundred fields). `--include tables.fields` is the full rollup — small databases only. When output outgrows the `--max-bytes` cap, the error message names the next command down the ladder. On warehouses with hundreds of tables, traverse by schema (`db schemas \u003cdb-id\u003e` → `db schema-tables \u003cdb-id\u003e \u003cschema\u003e`) or find tables by name (`mb search \u003cterm\u003e --models table --db-id \u003cdb-id\u003e`).\n\n### `mb db list`\n\n```sh\nmb db list\nmb db list --json\nmb db list --saved --json\nmb db list --include tables --json   # every db with its compact table map\n```\n\n| Flag                | Description                                                                                                                                                                             |\n| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `--include \u003cwhich\u003e` | Hydrate related entities. Currently only `tables` is supported (each database is returned with its compact `tables`). To map a single warehouse, prefer `db get \u003cid\u003e --include tables`. |\n| `--saved`           | Include the Saved Questions virtual database in the list. The virtual db has id `-1337` and no `engine`.                                                                                |\n\n### `mb db get \u003cid\u003e`\n\n```sh\nmb db get 1\nmb db get 1 --json\nmb db get 1 --include tables --json          # + compact table map (fits most databases)\nmb db get 1 --include tables.fields --json   # + every field of every table (small databases only)\n```\n\n| Flag                | Description                                                                                                                                                                                                                                                                                                     |\n| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `--include \u003cwhich\u003e` | Hydrate related entities. `tables` is the compact table map — the recommended first call for schema discovery. `tables.fields` is the full rollup and fits only small databases; on anything larger, take the map and fetch fields per table with `table fields \u003ctable-id\u003e`, or traverse by schema (see above). |\n\n### `mb db schemas \u003cid\u003e`\n\nList the schemas in a database. Schemas with no tables are excluded. Cheap and bounded — this is the right entry point for an agent walking a warehouse.\n\n```sh\nmb db schemas 1\nmb db schemas 1 --json\n```\n\n### `mb db schema-tables \u003cid\u003e \u003cschema\u003e`\n\nList the tables in one schema, sorted by display name. Returns compact projections without fields — pair with `table get --include fields` (or `table fields \u003cid\u003e`) per table you actually need to introspect.\n\n```sh\nmb db schema-tables 1 public\nmb db schema-tables 1 analytics --json\n```\n\n### `mb db sync-schema \u003cid\u003e`\n\nTrigger a manual schema sync (`POST /api/database/:id/sync_schema`). Returns `{ id, status: \"ok\" }` once the sync has been queued; the actual work happens asynchronously on the server. Pass `--wait` to poll the database until its `initial_sync_status` reports `complete` (a database that has already finished its initial sync returns at once). To wait for a specific newly-materialized transform table to register, prefer `mb transform run \u003cid\u003e --sync`.\n\n```sh\nmb db sync-schema 1\nmb db sync-schema 1 --wait --json\n```\n\n| Flag              | Description                                                 |\n| ----------------- | ----------------------------------------------------------- |\n| `--wait`          | Poll until `initial_sync_status` reports `complete`.        |\n| `--timeout \u003cms\u003e`  | Polling timeout in ms (default 600000). Used with `--wait`. |\n| `--interval \u003cms\u003e` | Polling interval in ms (default 2000). Used with `--wait`.  |\n\n### `mb db rescan-values \u003cid\u003e`\n\nTrigger a rescan of cached field values (`POST /api/database/:id/rescan_values`). Returns `{ id, status: \"ok\" }` once the rescan has been queued.\n\n```sh\nmb db rescan-values 1\nmb db rescan-values 1 --json\n```\n\n## Tables\n\nInspect and edit warehouse tables via `/api/table`. For agent-driven field introspection, `table get --include fields` is the default — it returns the table plus its columns in a single bounded response.\n\n### `mb table list`\n\nReturns every table in the chosen database (or across all databases) as a flat compact list — no fields, no per-table hydration. On a real warehouse with hundreds of tables this is still bounded (kilobytes), but `db schema-tables \u003cdb-id\u003e \u003cschema\u003e` is the better starting point when you know the schema.\n\n```sh\nmb table list\nmb table list --db-id 1 --json\n```\n\n| Flag           | Description                         |\n| -------------- | ----------------------------------- |\n| `--db-id \u003cid\u003e` | Filter tables by their database id. |\n\n### `mb table get \u003cid\u003e`\n\nReturns the basic table record (no fields). Pass `--include fields` to route through `/api/table/:id/query_metadata` so the response carries the table's columns compact-projected as `fields` — this is the default agent path for field introspection (the response also carries FK targets and dimensions under `--full`). Use `mb table fields \u003cid\u003e` if you only want the fields as a list envelope.\n\n```sh\nmb table get 42\nmb table get 42 --json\nmb table get 42 --include fields --json\n```\n\n| Flag                | Description                                                                                         |\n| ------------------- | --------------------------------------------------------------------------------------------------- |\n| `--include \u003cwhich\u003e` | Hydrate related entities. Currently only `fields` is supported (bundles compact-projected columns). |\n\n### `mb table fields \u003cid\u003e`\n\nList the fields on a table (a thin projection over `query_metadata.fields`). Use this when you want just the field array without the surrounding table metadata.\n\n```sh\nmb table fields 42\nmb table fields 42 --json\n```\n\n### `mb table update \u003cid\u003e`\n\nPatch a table (`PUT /api/table/:id`). Body fields: `display_name`, `description`, `caveats`, `points_of_interest`, `entity_type`, `visibility_type`, `field_order`, `show_in_getting_started`. Pass the body via `--body`, `--file`, or stdin (exactly one).\n\n```sh\nmb table update 42 --body '{\"display_name\":\"Customers\"}'\nmb table update 42 --file patch.json\necho '{\"description\":\"Customer dimension\"}' | mb table update 42\n```\n\nPublish status surfaces on the table itself — `table get`/`table list` carry `is_published` (and `collection_id` under `--full`). Publishing tables to the Library is done with [`mb library publish`](#library).\n\n## Fields\n\nInspect and edit individual columns via `/api/field`.\n\n### `mb field get \u003cid\u003e`\n\n```sh\nmb field get 100\nmb field get 100 --json\n```\n\n### `mb field values \u003cid\u003e`\n\nFetch the cached distinct values list (`GET /api/field/:id/values`). Returns the FieldValues envelope (`{ values, field_id, has_more_values }`); empty `values` on fields whose `has_field_values` is `none` or `search`.\n\n```sh\nmb field values 100 --json\n```\n\n### `mb field summary \u003cid\u003e`\n\nRow count and distinct count for the field (`GET /api/field/:id/summary`). Metabase returns this as an array-of-pairs; the CLI normalizes it to `{ field_id, count, distincts }`.\n\n```sh\nmb field summary 100\nmb field summary 100 --json\n```\n\n### `mb field update \u003cid\u003e`\n\nPatch a field (`PUT /api/field/:id`). Body fields: `display_name`, `description`, `caveats`, `points_of_interest`, `semantic_type`, `coercion_strategy`, `fk_target_field_id`, `visibility_type`, `has_field_values`, `settings`, `nfc_path`, `json_unfolding`. Pass the body via `--body`, `--file`, or stdin.\n\n```sh\nmb field update 100 --body '{\"description\":\"customer email\",\"semantic_type\":\"type/Email\"}'\nmb field update 100 --file patch.json\n```\n\n## Upload\n\nLoad CSV/TSV data into the warehouse via `/api/upload`. Requires an uploads database configured on the server (Admin → Settings → Uploads); the destination db and schema are set there, not per-command. `append`/`replace` target a table created by a prior upload, and the CSV columns must match.\n\n### `mb upload csv`\n\nCreate a new table plus a model over it from a CSV file. Prints the new model id and table id.\n\n```sh\nmb upload csv --file data.csv\nmb upload csv --file data.csv --collection 5\nmb upload csv --file data.csv --json\n```\n\n| Flag                | Description                                                            |\n| ------------------- | ---------------------------------------------------------------------- |\n| `--file \u003cpath\u003e`     | Path to the CSV/TSV file to upload (required).                         |\n| `--collection \u003cid\u003e` | Target collection id for the created model, or `root` (default: root). |\n\n### `mb upload append \u003ctable-id\u003e`\n\nInsert a CSV file's rows into an existing uploaded table.\n\n```sh\nmb upload append 42 --file more-rows.csv\nmb upload append 42 --file more-rows.csv --json\n```\n\n### `mb upload replace \u003ctable-id\u003e`\n\nReplace an existing uploaded table's contents with a CSV file's rows.\n\n```sh\nmb upload replace 42 --file rows.csv\nmb upload replace 42 --file rows.csv --json\n```\n\n## Cards\n\nCRUD plus query execution on `/api/card`. A \"card\" is a Metabase question, model, or metric. The `query` subcommand runs the card and either returns Metabase's JSON envelope or streams a raw CSV / XLSX export.\n\n### `mb card list`\n\n```sh\nmb card list\nmb card list --filter archived --json\nmb card list --filter using_model --model-id 42 --json\n```\n\n| Flag                | Description                                                                                                    |\n| ------------------- | -------------------------------------------------------------------------------------------------------------- |\n| `--filter \u003cpreset\u003e` | One of `all` (default), `mine`, `bookmarked`, `database`, `table`, `archived`, `using_model`, `using_segment`. |\n| `--model-id \u003cid\u003e`   | Required when `--filter` is `database`, `table`, `using_model`, or `using_segment`.                            |\n\n### `mb card get \u003cid\u003e`\n\n```sh\nmb card get 1\nmb card get 1 --json --full\n```\n\n### `mb card query \u003cid\u003e`\n\nRun the card's query. Without `--export-format`, returns the Metabase JSON envelope (`status`, `row_count`, `data: { rows, cols }`, …). With `--export-format csv`, `--export-format json`, or `--export-format xlsx`, the export bytes stream straight to stdout.\n\n```sh\nmb card query 1 --json\nmb card query 1 --json --limit 20\nmb card query 1 --export-format csv \u003e export.csv\nmb card query 1 --export-format json \u003e export.json\nmb card query 1 --export-format xlsx \u003e export.xlsx\nmb card query 1 --parameters '[{\"type\":\"category\",\"value\":\"A\",\"target\":[\"variable\",[\"template-tag\",\"c\"]]}]'\n```\n\n| Flag                    | Description                                                                                            |\n| ----------------------- | ------------------------------------------------------------------------------------------------------ |\n| `--export-format \u003cfmt\u003e` | Stream the export instead of the JSON envelope. One of `csv`, `json`, `xlsx`.                          |\n| `--parameters \u003cjson\u003e`   | JSON array of Metabase parameter objects (the same shape Metabase POSTs from a dashboard).             |\n| `--limit \u003cn\u003e`           | Cap rows kept in the JSON envelope. No effect on streamed exports.                                     |\n| `--format-rows`         | Streamed exports only: apply the card's visualization-settings formatting to values (default `false`). |\n| `--pivot-results`       | Streamed exports only: emit the pivoted output for pivot questions (default `false`).                  |\n\n### `mb card alerts \u003cid\u003e`\n\nList the alerts watching this card. Manage them with `mb alert create|update|send|archive`, which take the alert id printed here.\n\n```sh\nmb card alerts 94\nmb card alerts 94 --include-inactive --json\n```\n\n| Flag                 | Description                         |\n| -------------------- | ----------------------------------- |\n| `--include-inactive` | Include archived (inactive) alerts. |\n\n### `mb card create`\n\n```sh\ncat card.json | mb card create\nmb card create --file card.json\nmb card create --body '{\"name\":\"x\",\"display\":\"table\",\"dataset_query\":{...},\"visualization_settings\":{}}'\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\n### `mb card update \u003cid\u003e`\n\nPatch a card. Body is a partial subset of the create shape (`name`, `display`, `dataset_query`, `visualization_settings`, `description`, `archived`, `collection_id`, `dashboard_id`, `cache_ttl`, `parameters`, `parameter_mappings`, etc.). Only the keys you send are touched. If `dataset_query` is MBQL 5 (`lib/type: \"mbql/query\"`) it goes through the same pre-flight validation as `card create` and `mb query`; pass `--skip-validate` to bypass.\n\n```sh\ncat patch.json | mb card update 1\nmb card update 1 --file patch.json\nmb card update 1 --body '{\"name\":\"renamed\"}'\nmb card update 1 --body '{\"display\":\"bar\"}'\nmb card update 1 --body '{\"archived\":true}'\nmb card update 1 --file patch.json --skip-validate\n```\n\n| Flag              | Description                                                                                                                                            |\n| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `--body \u003cjson\u003e`   | Inline JSON body.                                                                                                                                      |\n| `--file \u003cpath\u003e`   | Path to JSON body file.                                                                                                                                |\n| `--skip-validate` | Skip the local MBQL 5 pre-flight validation; let the server be the authority. Use only when the bundled schema disagrees with what the server accepts. |\n\n### `mb card archive \u003cid\u003e`\n\nSoft-delete a card by setting `archived: true`. The archived card stays available via `card list --filter archived` and `card get \u003cid\u003e` until permanently deleted server-side. To unarchive (or otherwise toggle the flag) use `mb card update \u003cid\u003e --body '{\"archived\":false}'`.\n\n```sh\nmb card archive 1\nmb card archive 1 --json\n```\n\n## Dashboards\n\nRead and write dashboards on `/api/dashboard`. A dashboard groups cards (questions, models, metrics) into a single layout. Each card on a dashboard is a \"dashcard\" — a placement record with its own id, position (`row`/`col`), and size (`size_x`/`size_y`). Dashcards live nested inside the parent dashboard's `dashcards` array; the API has no per-dashcard endpoint, so single-dashcard edits round-trip through `PUT /api/dashboard/:id`.\n\n### `mb dashboard list`\n\n```sh\nmb dashboard list\nmb dashboard list --json\nmb dashboard list --filter archived --json\n```\n\n| Flag                | Description                                 |\n| ------------------- | ------------------------------------------- |\n| `--filter \u003cpreset\u003e` | One of `all` (default), `mine`, `archived`. |\n\n### `mb dashboard get \u003cid\u003e`\n\n```sh\nmb dashboard get 1\nmb dashboard get 1 --json\nmb dashboard get 1 --json --full\n```\n\n`--full` returns the full hydrated dashboard including the `dashcards` and `tabs` arrays. The default compact view returns only `id`, `name`, `description`, `archived`, and `collection_id`.\n\n### `mb dashboard cards \u003cid\u003e`\n\nList the dashcards on a dashboard.\n\n```sh\nmb dashboard cards 1\nmb dashboard cards 1 --json\n```\n\n### `mb dashboard subscriptions \u003cid\u003e`\n\nList the subscriptions delivering this dashboard. Manage them with `mb subscription create|update|archive`, which take the subscription id printed here.\n\n```sh\nmb dashboard subscriptions 10\nmb dashboard subscriptions 10 --json\n```\n\n| Flag         | Description                                         |\n| ------------ | --------------------------------------------------- |\n| `--archived` | Show archived subscriptions instead of active ones. |\n\n#### Dashboard parameters (filters)\n\nA dashboard's `parameters` are its filter widgets. They're typed (`Parameter` schema): an invalid `type` is rejected at the CLI boundary with a message that echoes the full allowed enum (`string/=`, `string/contains`, `number/between`, `date/range`, `category`, `id`, `temporal-unit`, …).\n\nRead them off the dashboard with `mb dashboard get \u003cid\u003e --fields parameters --json` (or `--full` for the whole record). There is no separate read verb — they're part of the dashboard.\n\nEditing replaces the **whole** `parameters` array, so it's a read-modify-write loop: read the current set, modify it, and send it all back via `mb dashboard create`/`mb dashboard update --body '{\"parameters\":[…]}'`; omitting a parameter deletes it. Each parameter's `id` is a descriptive string you choose (reuse the `slug`, e.g. `order_status`), unique within the dashboard — Metabase stores any non-blank string as-is, so there is no need to generate a random id (use `mb uuid` only if you genuinely want an opaque one). Bind a parameter to a card column through a dashcard's `parameter_mappings`, whose `parameter_id` must match a parameter `id` exactly.\n\n### `mb dashboard parameter-values \u003cdashboard-id\u003e \u003cparameter-id\u003e`\n\nFetch the selectable values for one dashboard parameter (`{values, has_more_values}`). Values come from the parameter's static list, its source card, or — for a parameter mapped to a field — the field's live distinct values (chain-filtered).\n\n```sh\nmb dashboard parameter-values 1 order_status --json\nmb dashboard parameter-values 1 order_status --query Cam --json\n```\n\n| Flag               | Description                                                                            |\n| ------------------ | -------------------------------------------------------------------------------------- |\n| `--query \u003csubstr\u003e` | Case-insensitive substring search (first 1000 matches) instead of the full value list. |\n\n### `mb dashboard create`\n\nThe body accepts the same dashboard-level fields as the underlying `POST /api/dashboard` (`name`, `description`, `parameters`, `cache_ttl`, `collection_id`, `collection_position`). It also accepts optional `dashcards` and `tabs`: when either is present, the CLI chains a `PUT /api/dashboard/:id` after the create and returns the updated dashboard with its dashcards/tabs applied. Use a negative `id` on a dashcard to indicate one the server should newly create.\n\n```sh\ncat dashboard.json | mb dashboard create\nmb dashboard create --file dashboard.json\nmb dashboard create --body '{\"name\":\"My Dashboard\",\"collection_id\":4}'\nmb dashboard create --body '{\"name\":\"D\",\"dashcards\":[{\"id\":-1,\"card_id\":42,\"row\":0,\"col\":0,\"size_x\":12,\"size_y\":6}]}'\n```\n\n| Flag            | Description                                         |\n| --------------- | --------------------------------------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.                                   |\n| `--file \u003cpath\u003e` | Path to JSON body file. Use `-` to read from stdin. |\n\n### `mb dashboard update \u003cid\u003e`\n\nPatch a dashboard. To edit the dashcard set, send the entire `dashcards` array — IDs not in the array get deleted, and a negative `id` indicates a new dashcard the server should create.\n\n```sh\ncat patch.json | mb dashboard update 1\nmb dashboard update 1 --file patch.json\nmb dashboard update 1 --body '{\"name\":\"renamed\"}'\nmb dashboard update 1 --body '{\"dashcards\":[{\"id\":-1,\"card_id\":42,\"row\":0,\"col\":0,\"size_x\":12,\"size_y\":6}]}'\n```\n\n### `mb dashboard update-dashcard \u003cdashboard-id\u003e \u003cdashcard-id\u003e`\n\nPatch a single dashcard's layout or settings. The command does the round-trip for you: `GET /api/dashboard/:id`, merges the patch into the targeted dashcard while preserving every other dashcard verbatim, then `PUT`s the whole array back.\n\n```sh\nmb dashboard update-dashcard 1 5 --body '{\"row\":2,\"col\":0}'\nmb dashboard update-dashcard 1 5 --body '{\"size_x\":12,\"size_y\":4}'\ncat patch.json | mb dashboard update-dashcard 1 5\n```\n\n| Patch field              | Type                               |\n| ------------------------ | ---------------------------------- |\n| `row`, `col`             | non-negative integer               |\n| `size_x`, `size_y`       | positive integer                   |\n| `dashboard_tab_id`       | integer or `null`                  |\n| `parameter_mappings`     | array of parameter-mapping objects |\n| `inline_parameters`      | array of strings                   |\n| `visualization_settings` | object                             |\n\nThe patch must contain at least one field; an empty object is rejected before the network round-trip.\n\n### `mb dashboard archive \u003cid\u003e`\n\nSoft-delete a dashboard by setting `archived: true`. The archived dashboard stays available via `dashboard list --filter archived` and `dashboard get \u003cid\u003e` until permanently deleted server-side. To unarchive use `mb dashboard update \u003cid\u003e --body '{\"archived\":false}'`.\n\n```sh\nmb dashboard archive 1\nmb dashboard archive 1 --json\n```\n\n## Snippets\n\nCRUD on `/api/native-query-snippet`. A snippet is a named, reusable piece of native (SQL) query text — referenced from cards via `{{snippet: Name}}`. The list endpoint returns either active or archived rows (mutually exclusive — pass `--archived` to swap).\n\n### `mb snippet list`\n\n```sh\nmb snippet list\nmb snippet list --json\nmb snippet list --archived --json\n```\n\n| Flag         | Description                                    |\n| ------------ | ---------------------------------------------- |\n| `--archived` | Show archived snippets instead of active ones. |\n\n### `mb snippet get \u003cid\u003e`\n\n```sh\nmb snippet get 1\nmb snippet get 1 --json --full\n```\n\n### `mb snippet create`\n\n```sh\ncat snippet.json | mb snippet create\nmb snippet create --file snippet.json\nmb snippet create --body '{\"name\":\"active\",\"content\":\"WHERE active = true\"}'\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\nBody fields: `name` (required), `content` (required), `description` (optional), `collection_id` (optional positive integer).\n\n### `mb snippet update \u003cid\u003e`\n\nPatch a snippet. Body is a partial subset of the create shape plus `archived`. Only the keys you send are touched.\n\n```sh\ncat patch.json | mb snippet update 1\nmb snippet update 1 --file patch.json\nmb snippet update 1 --body '{\"name\":\"renamed\"}'\nmb snippet update 1 --body '{\"archived\":true}'\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\n### `mb snippet archive \u003cid\u003e`\n\nSoft-delete a snippet by setting `archived: true`. To unarchive use `mb snippet update \u003cid\u003e --body '{\"archived\":false}'`.\n\n```sh\nmb snippet archive 1\nmb snippet archive 1 --json\n```\n\n## Segments\n\nCRUD on `/api/segment`. A segment is a saved MBQL filter macro tied to a table — used in card filters to share a reusable predicate. Mutating endpoints require a `revision_message` for the audit log.\n\n### `mb segment list`\n\n```sh\nmb segment list\nmb segment list --json\n```\n\n### `mb segment get \u003cid\u003e`\n\n```sh\nmb segment get 1\nmb segment get 1 --json --full\n```\n\n### `mb segment create`\n\n```sh\ncat segment.json | mb segment create\nmb segment create --file segment.json\nmb segment create --file segment.json --skip-validate\n```\n\n| Flag              | Description                                                                                                                                            |\n| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `--body \u003cjson\u003e`   | Inline JSON body.                                                                                                                                      |\n| `--file \u003cpath\u003e`   | Path to JSON body file.                                                                                                                                |\n| `--skip-validate` | Skip the local MBQL 5 pre-flight validation; let the server be the authority. Use only when the bundled schema disagrees with what the server accepts. |\n\nBody fields: `name` (required), `table_id` (required positive integer), `definition` (required MBQL filter object), `description` (optional). If `definition` is MBQL 5 (`lib/type: \"mbql/query\"`) it goes through the same pre-flight validation as `card create` and `mb query`; pass `--skip-validate` to bypass.\n\n### `mb segment update \u003cid\u003e`\n\nPatch a segment. The body MUST include `revision_message`. Other keys are partial: `name`, `definition`, `archived`, `description`, `caveats`, `points_of_interest`, `show_in_getting_started`. If `definition` is MBQL 5 (`lib/type: \"mbql/query\"`) it goes through the same pre-flight validation as `segment create`; pass `--skip-validate` to bypass.\n\n```sh\ncat patch.json | mb segment update 1\nmb segment update 1 --file patch.json\nmb segment update 1 --body '{\"name\":\"renamed\",\"revision_message\":\"rename\"}'\nmb segment update 1 --file patch.json --skip-validate\n```\n\n| Flag              | Description                                                                                                                                            |\n| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `--body \u003cjson\u003e`   | Inline JSON body.                                                                                                                                      |\n| `--file \u003cpath\u003e`   | Path to JSON body file.                                                                                                                                |\n| `--skip-validate` | Skip the local MBQL 5 pre-flight validation; let the server be the authority. Use only when the bundled schema disagrees with what the server accepts. |\n\n### `mb segment archive \u003cid\u003e`\n\nSoft-delete a segment by setting `archived: true`. The default revision message is `\"Archived via mb CLI\"`; override with `--revision-message`.\n\n```sh\nmb segment archive 1\nmb segment archive 1 --revision-message \"deprecated\"\n```\n\n| Flag                        | Description                                 |\n| --------------------------- | ------------------------------------------- |\n| `--revision-message \u003ctext\u003e` | Audit-log message recorded with the change. |\n\n## Measures\n\nCRUD on `/api/measure`. A measure is a saved MBQL aggregation (a single `:aggregation` clause) tied to a table — referenced from cards and metrics to share a reusable computation. Mutating endpoints require a `revision_message` for the audit log.\n\n### `mb measure list`\n\n```sh\nmb measure list\nmb measure list --json\n```\n\n### `mb measure get \u003cid\u003e`\n\n```sh\nmb measure get 1\nmb measure get 1 --json --full\n```\n\n### `mb measure create`\n\n```sh\ncat measure.json | mb measure create\nmb measure create --file measure.json\nmb measure create --file measure.json --skip-validate\n```\n\n| Flag              | Description                                                                                                                                            |\n| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `--body \u003cjson\u003e`   | Inline JSON body.                                                                                                                                      |\n| `--file \u003cpath\u003e`   | Path to JSON body file.                                                                                                                                |\n| `--skip-validate` | Skip the local MBQL 5 pre-flight validation; let the server be the authority. Use only when the bundled schema disagrees with what the server accepts. |\n\nBody fields: `name` (required), `table_id` (required positive integer), `definition` (required MBQL aggregation object), `description` (optional). If `definition` is MBQL 5 (`lib/type: \"mbql/query\"`) it goes through the same pre-flight validation as `card create` and `mb query`; pass `--skip-validate` to bypass.\n\n### `mb measure update \u003cid\u003e`\n\nPatch a measure. The body MUST include `revision_message`. Other keys are partial: `name`, `definition`, `archived`, `description`. If `definition` is MBQL 5 (`lib/type: \"mbql/query\"`) it goes through the same pre-flight validation as `measure create`; pass `--skip-validate` to bypass.\n\n```sh\ncat patch.json | mb measure update 1\nmb measure update 1 --file patch.json\nmb measure update 1 --body '{\"name\":\"renamed\",\"revision_message\":\"rename\"}'\nmb measure update 1 --file patch.json --skip-validate\n```\n\n| Flag              | Description                                                                                                                                            |\n| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `--body \u003cjson\u003e`   | Inline JSON body.                                                                                                                                      |\n| `--file \u003cpath\u003e`   | Path to JSON body file.                                                                                                                                |\n| `--skip-validate` | Skip the local MBQL 5 pre-flight validation; let the server be the authority. Use only when the bundled schema disagrees with what the server accepts. |\n\n### `mb measure archive \u003cid\u003e`\n\nSoft-delete a measure by setting `archived: true`. The default revision message is `\"Archived via mb CLI\"`; override with `--revision-message`.\n\n```sh\nmb measure archive 1\nmb measure archive 1 --revision-message \"deprecated\"\n```\n\n| Flag                        | Description                                 |\n| --------------------------- | ------------------------------------------- |\n| `--revision-message \u003ctext\u003e` | Audit-log message recorded with the change. |\n\n## Timelines\n\nCRUD on `/api/timeline`. A timeline is a named collection of dated events rendered as annotations on time-series charts. Timelines live in collections (`collection_id: null` = root) and carry an icon (`star`, `cake`, `mail`, `warning`, `bell`, `cloud`).\n\n### `mb timeline list`\n\n```sh\nmb timeline list\nmb timeline list --json\nmb timeline list --archived --json\n```\n\n| Flag         | Description                                     |\n| ------------ | ----------------------------------------------- |\n| `--archived` | Show archived timelines instead of active ones. |\n\n### `mb timeline get \u003cid\u003e`\n\n```sh\nmb timeline get 1\nmb timeline get 1 --json --full\n```\n\n### `mb timeline events \u003cid\u003e`\n\nList the events on a timeline. Archived events are excluded unless `--archived` is passed (which returns both).\n\n```sh\nmb timeline events 1\nmb timeline events 1 --archived --json\n```\n\n| Flag         | Description              |\n| ------------ | ------------------------ |\n| `--archived` | Include archived events. |\n\n### `mb timeline create`\n\n```sh\nmb timeline create --body '{\"name\":\"Releases\"}'\ncat timeline.json | mb timeline create\nmb timeline create --file timeline.json\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\nBody fields: `name` (required), `description` (optional), `icon` (optional, default `star`), `collection_id` (optional positive integer, omit for the root collection), `default` (optional boolean marking the collection's default timeline).\n\n### `mb timeline update \u003cid\u003e`\n\nPatch a timeline. Body is a partial subset of the create shape plus `archived`. Only the keys you send are touched. Changing `archived` cascades to every event on the timeline.\n\n```sh\nmb timeline update 1 --body '{\"name\":\"Product releases\"}'\ncat patch.json | mb timeline update 1\nmb timeline update 1 --file patch.json\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\n### `mb timeline archive \u003cid\u003e`\n\nSoft-delete a timeline (and, by server-side cascade, all its events) by setting `archived: true`. To unarchive use `mb timeline update \u003cid\u003e --body '{\"archived\":false}'`.\n\n```sh\nmb timeline archive 1\nmb timeline archive 1 --json\n```\n\n### `mb timeline delete \u003cid\u003e`\n\nPermanently delete a timeline and all its events. Irreversible — prefer `mb timeline archive` unless you mean it. Prompts for confirmation on a TTY; requires `--yes` otherwise.\n\n```sh\nmb timeline delete 1 --yes\nmb timeline delete 1\n```\n\n| Flag    | Description        |\n| ------- | ------------------ |\n| `--yes` | Skip confirmation. |\n\n## Timeline events\n\nCRUD on `/api/timeline-event`. An event is a dated annotation on a timeline. There is no server-side list endpoint — list events with `mb timeline events \u003cid\u003e`.\n\n### `mb timeline-event get \u003cid\u003e`\n\n```sh\nmb timeline-event get 1\nmb timeline-event get 1 --json --full\n```\n\n### `mb timeline-event create`\n\n```sh\nmb timeline-event create --body '{\"name\":\"v2 launch\",\"timestamp\":\"2026-07-01T00:00:00Z\",\"timezone\":\"UTC\",\"time_matters\":false,\"timeline_id\":1}'\ncat event.json | mb timeline-event create\nmb timeline-event create --file event.json\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\nBody fields: `name` (required), `timestamp` (required, ISO 8601), `timezone` (required, IANA name like `UTC` or `America/New_York`), `time_matters` (required boolean — `true` when the time of day is significant, `false` when only the date is), `timeline_id` (required positive integer), `description` (optional), `icon` (optional, default: the timeline's icon).\n\n### `mb timeline-event update \u003cid\u003e`\n\nPatch an event. Body is a partial subset of the create shape plus `archived`. Only the keys you send are touched; `timeline_id` moves the event to another timeline.\n\n```sh\nmb timeline-event update 1 --body '{\"name\":\"v2.1 launch\"}'\ncat patch.json | mb timeline-event update 1\nmb timeline-event update 1 --file patch.json\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\n### `mb timeline-event archive \u003cid\u003e`\n\nSoft-delete an event by setting `archived: true`. To unarchive use `mb timeline-event update \u003cid\u003e --body '{\"archived\":false}'`.\n\n```sh\nmb timeline-event archive 1\nmb timeline-event archive 1 --json\n```\n\n### `mb timeline-event delete \u003cid\u003e`\n\nPermanently delete an event. Prompts for confirmation on a TTY; requires `--yes` otherwise.\n\n```sh\nmb timeline-event delete 1 --yes\nmb timeline-event delete 1\n```\n\n| Flag    | Description        |\n| ------- | ------------------ |\n| `--yes` | Skip confirmation. |\n\n## Dashboard subscriptions\n\nRead and write dashboard subscriptions on `/api/pulse`. A subscription delivers a rendered dashboard on a schedule — by email, to a Slack channel, or to an HTTP webhook. It pins the dashboard's cards by both `id` (the card) and `dashboard_card_id` (its placement); `mb dashboard cards \u003cdashboard-id\u003e` prints both.\n\nA subscription's `dashboard_id` and `collection_id` are fixed at creation. There is no delete — archiving is the terminal state, and it also disables every channel.\n\n### `mb subscription list`\n\n```sh\nmb subscription list\nmb subscription list --dashboard-id 10 --json\nmb subscription list --archived --json\n```\n\n| Flag                  | Description                                         |\n| --------------------- | --------------------------------------------------- |\n| `--dashboard-id \u003cid\u003e` | Only subscriptions on this dashboard.               |\n| `--archived`          | Show archived subscriptions instead of active ones. |\n\nListing from the dashboard side is `mb dashboard subscriptions \u003cdashboard-id\u003e`.\n\n### `mb subscription get \u003cid\u003e`\n\n```sh\nmb subscription get 1\nmb subscription get 1 --full --json\n```\n\nThe compact view returns `id`, `name`, `dashboard_id`, `collection_id`, `archived`, `skip_if_empty`, plus the pinned `cards` and the `channels` with their schedules and recipients. `--full` adds the hydrated creator, entity ids, and per-card download permissions.\n\n### `mb subscription create`\n\nThe body needs `name`, `dashboard_id`, `cards`, and `channels`.\n\nEach channel names a `channel_type` (`email`, `slack`, `http`) and a `schedule_type` (`hourly`, `daily`, `weekly`, `monthly`) plus the fields that schedule needs: `daily` needs `schedule_hour` (0–23); `weekly` also needs `schedule_day` (`mon`…`sun`); `monthly` also needs `schedule_frame` (`first`, `mid`, `last`). Email recipients are `{\"email\":\"a@b.com\"}` or `{\"id\":\u003cuser-id\u003e}`; Slack targets a channel with `\"details\":{\"channel\":\"#general\"}`. A channel is `enabled` unless you say otherwise.\n\n```sh\nmb subscription create --body '{\"name\":\"Weekly orders\",\"dashboard_id\":10,\"cards\":[{\"id\":94,\"dashboard_card_id\":87,\"include_csv\":false,\"include_xls\":false}],\"channels\":[{\"channel_type\":\"email\",\"schedule_type\":\"daily\",\"schedule_hour\":8,\"recipients\":[{\"email\":\"team@example.com\"}]}]}'\ncat subscription.json | mb subscription create\nmb subscription create --file subscription.json\n```\n\n| Flag            | Description                                         |\n| --------------- | --------------------------------------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.                                   |\n| `--file \u003cpath\u003e` | Path to JSON body file. Use `-` to read from stdin. |\n\n### `mb subscription update \u003cid\u003e`\n\nPatches `name`, `cards`, `channels`, `skip_if_empty`, `parameters`, `archived`. `cards` and `channels` each replace the whole list, so send every one you want to keep — `mb subscription get \u003cid\u003e --full` prints the current set.\n\nThe update reads the subscription first and carries `archived` and `skip_if_empty` forward when your patch omits them. That is load-bearing: `PUT /api/pulse/:id` defaults every omitted key, and both of those default to `false`, so a raw name-only PUT would un-archive the subscription and clear `skip_if_empty`.\n\n```sh\nmb subscription update 1 --body '{\"name\":\"Daily orders\"}'\nmb subscription update 1 --body '{\"channels\":[{\"channel_type\":\"email\",\"schedule_type\":\"weekly\",\"schedule_hour\":8,\"schedule_day\":\"mon\",\"recipients\":[{\"email\":\"team@example.com\"}]}]}'\nmb subscription update 1 --file patch.json\n```\n\n| Flag            | Description                                         |\n| --------------- | --------------------------------------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.                                   |\n| `--file \u003cpath\u003e` | Path to JSON body file. Use `-` to read from stdin. |\n\n### `mb subscription archive \u003cid\u003e`\n\nArchive a subscription, stopping all deliveries. Also disables every channel on it, so restoring means un-archiving and then re-enabling the channels.\n\n```sh\nmb subscription archive 1\nmb subscription archive 1 --json\n```\n\n## Question alerts\n\nRead and write question alerts on `/api/notification`. An alert watches one card and delivers it when a send condition fires on a schedule: `has_result` (the card returned any row), or `goal_above` / `goal_below` (both need a goal set on the card's visualization).\n\nSchedules are Quartz cron strings — `0 0 8 * * ? *` is daily at 08:00. `/api/notification` also carries Metabase's internal system-event notifications; `mb alert` scopes every request to card alerts, so they never appear.\n\nArchiving deactivates an alert rather than deleting it: `mb alert list --include-inactive` still finds it, and `mb alert update \u003cid\u003e --body '{\"active\":true}'` brings it back.\n\n### `mb alert list`\n\n```sh\nmb alert list\nmb alert list --card-id 94 --json\nmb alert list --include-inactive --json\n```\n\n| Flag                  | Description                         |\n| --------------------- | ----------------------------------- |\n| `--card-id \u003cid\u003e`      | Only alerts watching this card.     |\n| `--creator-id \u003cid\u003e`   | Only alerts created by this user.   |\n| `--recipient-id \u003cid\u003e` | Only alerts delivered to this user. |\n| `--include-inactive`  | Include archived (inactive) alerts. |\n\nListing from the question side is `mb card alerts \u003ccard-id\u003e`.\n\n### `mb alert get \u003cid\u003e`\n\n```sh\nmb alert get 9\nmb alert get 9 --full --json\n```\n\nThe compact view returns `id`, `active`, `creator_id`, the `payload` (`card_id`, `send_condition`, `send_once`), the cron `subscriptions`, and the `handlers` with their recipients. `--full` adds the hydrated card the alert watches.\n\n### `mb alert create`\n\nThe body needs `payload`, `subscriptions`, and `handlers`. Each handler names a `channel_type` (`channel/email`, `channel/slack`, `channel/http`) and its `recipients`; a recipient is `{\"type\":\"notification-recipient/user\",\"user_id\":3}` or `{\"type\":\"notification-recipient/raw-value\",\"details\":{\"value\":\"a@b.com\"}}`.\n\n```sh\nmb alert create --body '{\"payload\":{\"card_id\":94,\"send_condition\":\"has_result\"},\"subscriptions\":[{\"cron_schedule\":\"0 0 8 * * ? *\"}],\"handlers\":[{\"channel_type\":\"channel/email\",\"recipients\":[{\"type\":\"notification-recipient/raw-value\",\"details\":{\"value\":\"team@example.com\"}}]}]}'\ncat alert.json | mb alert create\nmb alert create --file alert.json\n```\n\n| Flag            | Description                                         |\n| --------------- | --------------------------------------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.                                   |\n| `--file \u003cpath\u003e` | Path to JSON body file. Use `-` to read from stdin. |\n\n### `mb alert update \u003cid\u003e`\n\nPatches the top-level fields you send: `payload`, `subscriptions`, `handlers`, `active`. Fields inside `payload` merge over the current ones, so `{\"payload\":{\"send_condition\":\"goal_above\"}}` keeps the card. `subscriptions` and `handlers` each replace the whole list — `mb alert get \u003cid\u003e` prints the current set. An alert cannot be moved to a different card.\n\nThe update reads the alert first and merges your patch over it. That is load-bearing: `PUT /api/notification/:id` is a spec-diff, and a body whose `id` doesn't match the stored one makes Metabase delete the alert and insert a replacement under a fresh id.\n\n```sh\nmb alert update 9 --body '{\"payload\":{\"send_condition\":\"goal_above\"}}'\nmb alert update 9 --body '{\"subscriptions\":[{\"cron_schedule\":\"0 0 9 * * ? *\"}]}'\nmb alert update 9 --body '{\"active\":true}'\n```\n\n| Flag            | Description                                         |\n| --------------- | --------------------------------------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.                                   |\n| `--file \u003cpath\u003e` | Path to JSON body file. Use `-` to read from stdin. |\n\n### `mb alert send \u003cid\u003e`\n\nSend an alert now, off-schedule. Delivers to every handler, ignoring the send condition. The channel must be configured on the server (email needs SMTP set up).\n\n```sh\nmb alert send 9\nmb alert send 9 --json\n```\n\n### `mb alert archive \u003cid\u003e`\n\nArchive an alert, stopping all deliveries and dropping its scheduled trigger.\n\n```sh\nmb alert archive 9\nmb alert archive 9 --json\n```\n\n## Collections\n\nRead collections on `/api/collection`. Collections are the folders that contain cards, dashboards, and other collections. The list endpoint surfaces a virtual root collection (id `\"root\"`) alongside regular numeric ids; the get endpoint accepts only the numeric id.\n\n### `mb collection list`\n\n```sh\nmb collection list\nmb collection list --json\nmb collection list --filter archived --json\n```\n\n| Flag                | Description                                                                                                     |\n| ------------------- | --------------------------------------------------------------------------------------------------------------- |\n| `--filter \u003cpreset\u003e` | One of `all` (default), `archived` (returns the trash collection only), `personal` (only personal collections). |\n\n### `mb collection get \u003cid\u003e`\n\n`\u003cid\u003e` accepts any of: a positive integer collection id, the literal `root` (the virtual \"Our analytics\" root), the literal `trash` (the trash collection), or a 21-character entity id (NanoID). Anything else is rejected with a `ConfigError` before any HTTP call.\n\n```sh\nmb collection get 4\nmb collection get root --json\nmb collection get trash --json\nmb collection get voo1If9y8Sld0lXej6xl0 --json\nmb collection get 4 --json --full\n```\n\n`--full` returns the full hydrated collection including `slug`, `entity_id`, `can_write`, `namespace`, and `personal_owner_id`. The default compact view returns `id`, `name`, `description`, `archived`, `location`, `parent_id`, `type`, `authority_level`, and `is_personal`. The root collection has a stripped-down shape — `archived`, `description`, `location`, `type`, etc. are absent rather than `null`.\n\n### `mb collection items \u003cid\u003e`\n\nList the cards, dashboards, sub-collections, and other content stored inside a collection. The CLI drains all pages of `/api/collection/:id/items`; pass `--limit` to cap the result. `\u003cid\u003e` accepts the same forms as `collection get` — including `root` for top-level content (items there have `collection_id: null`).\n\n```sh\nmb collection items 4\nmb collection items root --json\nmb collection items 4 --models card,dashboard --json\nmb collection items 4 --pinned-state is_pinned --json\n```\n\n| Flag                     | Description                                                                                                        |\n| ------------------------ | ------------------------------------------------------------------------------------------------------------------ |\n| `--models \u003ccsv\u003e`         | Restrict to one or more models (`card`, `dataset`, `metric`, `dashboard`, `snippet`, `collection`, `document`, …). |\n| `--archived`             | Return archived items instead of unarchived.                                                                       |\n| `--pinned-state \u003cstate\u003e` | One of `all`, `is_pinned`, `is_not_pinned`.                                                                        |\n| `--limit \u003cn\u003e`            | Cap total items returned. Default: drain all pages.                                                                |\n\n### `mb collection tree`\n\nFetch the full collection hierarchy as a nested tree. Output is always JSON — the recursive structure does not render meaningfully as a key/value table.\n\n```sh\nmb collection tree\nmb collection tree --json\n```\n\n### `mb collection create`\n\nCreate a collection from a JSON spec. The body accepts the same fields as `POST /api/collection`: `name` (required), `description`, `parent_id` (omit or `null` for the root), `namespace`, and `authority_level`.\n\n```sh\ncat collection.json | mb collection create\nmb collection create --file collection.json\nmb collection create --body '{\"name\":\"My Collection\",\"parent_id\":4}'\nmb collection create --body '{\"name\":\"ETL\"}' --namespace transforms\n```\n\n| Flag               | Description                                                                                                                                                                                                      |\n| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `--body \u003cjson\u003e`    | Inline JSON body.                                                                                                                                                                                                |\n| `--file \u003cpath\u003e`    | Path to JSON body file. Use `-` to read from stdin.                                                                                                                                                              |\n| `--namespace \u003cns\u003e` | Collection namespace (`transforms`, `snippets`, `analytics`, `shared-tenant-collection`, `tenant-specific`). Omit for a normal collection; required for a collection a transform's `collection_id` can point at. |\n\n### `mb collection archive \u003cid\u003e`\n\nSoft-delete a collection by setting `archived: true`. The archived collection stays available via `collection list --filter archived` until permanently deleted server-side. Restore it from the trash in the Metabase UI.\n\n```sh\nmb collection archive 4\nmb collection archive 4 --json\n```\n\n## Library\n\nCurate the Metabase **Library** — a governed subtree (`library-data` \"Data\" for published tables, `library-metrics` \"Metrics\" for official metrics, under a `library` root). Tables published to Data appear first when people pick a data source and rank up in search, steering everyone toward trusted, analysis-ready tables. Requires the `library` premium feature (Pro/Enterprise) and admin or data-analyst permission (Curate alone won't publish tables). Publish status surfaces on the table via `is_published` (`table get`/`table list`).\n\n### `mb library get`\n\nShow the Library and its Data/Metrics collection ids (`GET /api/ee/library/`). Errors if the Library hasn't been created yet.\n\n```sh\nmb library get\nmb library get --json\n```\n\n### `mb library create`\n\nCreate the Library subtree (`POST /api/ee/library/`). Idempotent — returns the existing Library when it's already there.\n\n```sh\nmb library create\nmb library create --json\n```\n\n### `mb library publish`\n\nPublish tables (and their upstream dependencies) into the Library's Data collection (`POST /api/ee/data-studio/table/publish-tables`). The target Data collection is resolved automatically and the Library is created if it doesn't exist yet — there's no collection id to pass. Publishing does not add the Data collection to the git-sync scope; on an instance with remote sync configured, the command warns on stderr with the `mb git-sync add-collection \u003cid\u003e` invocation that makes exports carry the published tables' metadata.\n\n```sh\nmb library publish --table-ids 1,2,3\nmb library publish --db-ids 1 --json\nmb library publish --schemas 1:public,1:analytics\n```\n\n| Flag                | Description                                                       |\n| ------------------- | ----------------------------------------------------------------- |\n| `--table-ids \u003cids\u003e` | Comma-separated table ids.                                        |\n| `--db-ids \u003cids\u003e`    | Comma-separated database ids.                                     |\n| `--schemas \u003cids\u003e`   | Comma-separated schema ids, each `\u003cdb-id\u003e:\u003cschema\u003e` (`1:public`). |\n\n### `mb library unpublish`\n\nUnpublish tables (and their downstream dependents) from the Library (`POST /api/ee/data-studio/table/unpublish-tables`). Same selector flags as `publish`.\n\n```sh\nmb library unpublish --table-ids 1,2,3\nmb library unpublish --db-ids 1 --json\n```\n\n## Documents\n\nCRUD on `/api/document`. A document is a rich-text page that mixes prose with embedded saved questions (`cardEmbed`) and inline links to Metabase entities (`smartLink`). The body is a [TipTap](https://tiptap.dev/) (ProseMirror) JSON tree stored under `content_type: application/json+vnd.prose-mirror`. The agent-facing format reference lives in the bundled `document` skill (`mb skills get document`). It's a baseline OSS feature — no elevated server version or premium token required.\n\n### `mb document list`\n\nReturns non-archived documents visible to you. The compact item omits the (potentially large) `document` body — pull it with `get --full`.\n\n```sh\nmb document list\nmb document list --json\n```\n\n### `mb document get \u003cid\u003e`\n\n```sh\nmb document get 1\nmb document get 1 --json --full\n```\n\n### `mb document create`\n\n```sh\ncat document.json | mb document create\nmb document create --file document.json\nmb document create --body '{\"name\":\"Notes\",\"document\":{\"type\":\"doc\",\"content\":[]}}'\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\nBody fields: `name` (required), `document` (required — the TipTap `doc` tree), `collection_id` (optional positive integer; `null` files it under \"Our analytics\"), `collection_position` (optional positive integer). New cards can be created inline by referencing them with negative ids in `cardEmbed` nodes and supplying their definitions in a top-level `cards` map — see the `document` skill.\n\nFor a document to open clean (no spurious \"unsaved changes\"), each id-bearing node (`paragraph`, `heading`, `codeBlock`, `orderedList`, `bulletList`, `blockquote`, `cardEmbed`, `supportingText`) must carry a unique `_id` — `create`/`update` **validate** this and reject a body where any such node is missing one (mint ids with `mb uuid`). Other node types don't take an `_id`. See the `document` skill (`mb skills get document`) for the full authoring guide.\n\n### `mb document update \u003cid\u003e`\n\nPatch a document. Body is a partial subset of the create shape plus `archived`. Only the keys you send are touched; replacing `document` replaces the whole body.\n\n```sh\ncat patch.json | mb document update 1\nmb document update 1 --file patch.json\nmb document update 1 --body '{\"name\":\"renamed\"}'\nmb document update 1 --body '{\"archived\":false}'\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\n### `mb document archive \u003cid\u003e`\n\nSoft-delete a document by setting `archived: true`. To unarchive use `mb document update \u003cid\u003e --body '{\"archived\":false}'`.\n\n```sh\nmb document archive 1\nmb document archive 1 --json\n```\n\n## Settings\n\nRead and write Metabase instance settings via `/api/setting`. Listing all settings requires admin privileges; per-key reads/writes additionally enforce per-setting access. Setting values are always JSON — `\"main\"` is the string `main`, `42` is a number, `null` deletes the override and resets the value to its default.\n\n### `mb setting list`\n\n```sh\nmb setting list\nmb setting list --json --max-bytes 0\n```\n\nReturns a `ListEnvelope` of compact entries (`key`, `value`, `is_env_setting`, `env_name`). Pass `--full` for the full per-row payload (also includes `description` and `default`). The full payload can exceed the default `--max-bytes` cap; pass `--max-bytes 0` to disable the cap.\n\n### `mb setting get \u003ckey\u003e`\n\n```sh\nmb setting get site-name\nmb setting get remote-sync-branch --json\n```\n\nReturns `{ key, value }` for a single setting. Settings whose stored value matches the default — or that come from an env var — surface as `value: null`.\n\n### `mb setting set \u003ckey\u003e [value]`\n\nSet or delete a setting. The value is parsed strictly as JSON: pass `'\"main\"'` for the string `main`, `true`/`42` for booleans/numbers, `null` to delete the stored override (resets to default).\n\n```sh\nmb setting set remote-sync-branch '\"main\"'\nmb setting set anon-tracking-enabled true\necho '\"main\"' | mb setting set remote-sync-branch\nmb setting set remote-sync-branch --file value.json\nmb setting set remote-sync-branch null\n```\n\n| Flag            | Description                                                              |\n| --------------- | ------------------------------------------------------------------------ |\n| `--file \u003cpath\u003e` | Read the JSON value from a file (alternative to the positional / stdin). |\n\nSources are resolved in this order: positional, `--file`, piped stdin. Provide exactly one; an unparseable value or a missing source fails fast with a `ConfigError`.\n\n## Search\n\n### `mb search [query]`\n\nSearch Metabase content (cards, dashboards, collections, tables, …). Returns a `ListEnvelope` of compact search results by default; pass `--full` for the full per-row payload.\n\n```sh\nmb search orders\nmb search --models card,dashboard --limit 10 --json\nmb search products --archived\n```\n\n| Flag             | Description                                                                                                                                               |\n| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `--models`, `-m` | Comma-separated model filter: `card,dataset,metric,dashboard,collection,database,table,segment,measure,snippet,document,action,transform,indexed-entity`. |\n| `--archived`     | Include archived items only.                                                                                                                              |\n| `--limit`        | Max results to return (default `20`).                                                                                                                     |\n| `--db-id`        | Restrict to items on a given database id.                                                                                                                 |\n| `--verified`     | Only verified content.                                                                                                                                    |\n\n## Git Sync\n\nDrive Metabase Enterprise Remote Sync (`/api/ee/remote-sync`) — import / export Metabase content against a configured git remote, inspect dirty state, and manage branches. All git-sync commands require an active EE token and superuser credentials.\n\n### `mb git-sync status`\n\nRoll up the current sync state in one call: configured branch, dirty flag, the most recent sync task (or `null` if none has ever run), and the collections marked for sync.\n\n```sh\nmb git-sync status\nmb git-sync status --json\n```\n\n### `mb git-sync is-dirty`\n\nBoolean check for whether any synced collection has unsynced local changes.\n\n```sh\nmb git-sync is-dirty --json\n```\n\n### `mb git-sync has-remote-changes`\n\nCompare the latest version on the remote branch against the version Metabase last imported. Cached for a short TTL server-side; pass `--force-refresh` to bypass.\n\n```sh\nmb git-sync has-remote-changes\nmb git-sync has-remote-changes --force-refresh --json\n```\n\n| Flag              | Description                                         |\n| ----------------- | --------------------------------------------------- |\n| `--force-refresh` | Bypass the in-memory cache and re-check the remote. |\n\n### `mb git-sync dirty`\n\nList every object that has unsynced local changes (compact list envelope; `--full` for the per-row payload).\n\n```sh\nmb git-sync dirty\nmb git-sync dirty --json\n```\n\n### `mb git-sync current-task`\n\nFetch the most recent sync task. Renders `{ status: \"idle\" }` when no task has ever run, otherwise the full task with its hydrated `status`.\n\n```sh\nmb git-sync current-task\nmb git-sync current-task --json\n```\n\n### `mb git-sync cancel-task`\n\nCancel the currently running sync task. Fails with HTTP 400 if no task is running.\n\n```sh\nmb git-sync cancel-task --json\n```\n\n### `mb git-sync wait`\n\nPoll `/current-task` until it reaches a terminal status (`successful`, `errored`, `cancelled`, `timed-out`, `conflict`). Exits 0 on `successful` or `cancelled`; exits 1 on `errored` / `timed-out` / `conflict`. Returns immediately with `{ status: \"idle\" }` if no task is running.\n\n```sh\nmb git-sync wait\nmb git-sync wait --timeout 300000 --json\n```\n\n| Flag              | Description                             |\n| ----------------- | --------------------------------------- |\n| `--timeout \u003cms\u003e`  | Polling timeout in ms (default 600000). |\n| `--interval \u003cms\u003e` | Polling interval in ms (default 2000).  |\n\n### `mb git-sync import`\n\nImport content from the configured git remote into Metabase (repo → Metabase). Auto-polls until the resulting task reaches a terminal status; pass `--no-wait` to return immediately after kickoff.\n\n```sh\nmb git-sync import\nmb git-sync import --branch main --json\nmb git-sync import --force --no-wait\n```\n\n| Flag                    | Description                                                           |\n| ----------------------- | --------------------------------------------------------------------- |\n| `--branch \u003cname\u003e`, `-b` | Branch to import from (defaults to the `remote-sync-branch` setting). |\n| `--force`               | Discard local Metabase-side dirty changes before importing (LOSSY).   |\n| `--wait` / `--no-wait`  | Poll until the task reaches a terminal status (default: wait).        |\n| `--timeout \u003cms\u003e`        | Polling timeout in ms (default 600000). Used with `--wait`.           |\n| `--interval \u003cms\u003e`       | Polling interval in ms (default 2000). Used with `--wait`.            |\n\n### `mb git-sync export`\n\nExport Metabase changes back to the configured git remote (Metabase → repo). Auto-polls by default.\n\n```sh\nmb git-sync export -m \"update dashboards\"\nmb git-sync export --branch main --json\nmb git-sync export --no-wait\n```\n\n| Flag                    | Description                                                         |\n| ----------------------- | ------------------------------------------------------------------- |\n| `--branch \u003cname\u003e`, `-b` | Branch to export to (defaults to the `remote-sync-branch` setting). |\n| `--message \u003cmsg\u003e`, `-m` | Commit message for the export.                                      |\n| `--force`               | Force-push / overwrite the remote branch.                           |\n| `--wait` / `--no-wait`  | Poll until the task reaches a terminal status (default: wait).      |\n| `--timeout \u003cms\u003e`        | Polling timeout in ms (default 600000). Used with `--wait`.         |\n| `--interval \u003cms\u003e`       | Polling interval in ms (default 2000). Used with `--wait`.          |\n\n### `mb git-sync stash`\n\nExport the current Metabase state to a NEW branch on the remote and switch sync to it. Requires `remote-sync-type` to be `read-write`.\n\n```sh\nmb git-sync stash --new-branch wip\nmb git-sync stash --new-branch wip -m \"work in progress\" --json\n```\n\n| Flag                    | Description                                                    |\n| ----------------------- | -------------------------------------------------------------- |\n| `--new-branch \u003cname\u003e`   | Required. Branch to create and export to.                      |\n| `--message \u003cmsg\u003e`, `-m` | Commit message (default `Stashed from mb CLI`).                |\n| `--wait` / `--no-wait`  | Poll until the task reaches a terminal status (default: wait). |\n| `--timeout \u003cms\u003e`        | Polling timeout in ms. Used with `--wait`.                     |\n| `--interval \u003cms\u003e`       | Polling interval in ms. Used with `--wait`.                    |\n\n### `mb git-sync branches`\n\nList branches available on the configured git remote.\n\n```sh\nmb git-sync branches --json\n```\n\n### `mb git-sync create-branch \u003cname\u003e`\n\nCreate a new branch on the git remote (from the last imported version) and switch sync to it.\n\n```sh\nmb git-sync create-branch feat/dashboards\nmb git-sync create-branch feat/x --json\n```\n\n### `mb git-sync add-collection \u003cid\u003e`\n\nMark a collection as git-synced. The toggle cascades to every descendant by `location` prefix, so flagging a parent flags the whole subtree. Returns `{ success, task_id? }`; `task_id` only appears when the toggle triggers a follow-up task (e.g. a finalization import after switching to read-only mode).\n\n```sh\nmb git-sync add-collection 12\nmb git-sync add-collection 12 --json --profile prod\n```\n\nThe server rejects toggles while `remote-sync-type` is `read-only` (the install default). Switch first with `mb setting set remote-sync-type '\"read-write\"'`.\n\n### `mb git-sync remove-collection \u003cid\u003e`\n\nUnmark a collection as git-synced. Same cascade and same `read-only` precondition as `add-collection`.\n\n```sh\nmb git-sync remove-collection 12\nmb git-sync remove-collection 12 --json --profile prod\n```\n\n## Instance setup\n\nBootstrapping a fresh, not-yet-configured Metabase instance.\n\n### `mb setup`\n\nComplete the initial setup wizard (`POST /api/setup`). The body must include the setup token, the default user, and the `prefs` block (with `site_name`).\n\n```sh\ncat setup.json | mb setup\nmb setup --file setup.json\nmb setup --body '{\"token\":\"\u003csetup-token\u003e\",\"user\":{\"email\":\"a@b.c\",\"password\":\"...\"},\"prefs\":{\"site_name\":\"Acme\"}}'\n```\n\n| Flag            | Description             |\n| --------------- | ----------------------- |\n| `--body \u003cjson\u003e` | Inline JSON body.       |\n| `--file \u003cpath\u003e` | Path to JSON body file. |\n\n## Agent helpers\n\nEndpoints commonly used by agents driving the instance. `card query` and `transform run` are documented in their own sections; the helper below covers entity-id translation.\n\n### `mb eid [eids]`\n\nTranslate string entity ids (EIDs) to numeric ids (`POST /api/eid-translation/translate`).\n\n```sh\nmb eid --model card abc123XYZ,def456ABC\nmb eid --file translate.json\nmb eid --body '{\"entity_ids\":{\"card\":[\"abc123XYZ\"]}}'\n```\n\nEntity ids are NanoIDs that can start with `-`, which the positional `\u003ceids\u003e` form misreads as a flag (shell quoting doesn't help — the leading `-` survives into argv). For an id that may start with `-`, pass it via `--body`, where the id is a JSON string value immune to flag parsing: `mb eid --body '{\"entity_ids\":{\"card\":[\"-abc123XYZ\"]}}'`.\n\n| Arg / Flag       | Description                                                                    |\n| ---------------- | ------------------------------------------------------------------------------ |\n| `\u003ceids\u003e`         | Comma-separated EIDs positional. Used with `--model`.                          |\n| `--model \u003cname\u003e` | Entity model for the positional EIDs (e.g. `card`, `dashboard`, `collection`). |\n| `--body \u003cjson\u003e`  | Inline JSON body.                                                              |\n| `--file \u003cpath\u003e`  | Path to JSON body file.                                                        |\n\n## Query\n\n### `mb query`\n\nRun an MBQL 5 query with built-in schema validation. Three modes — discover the schema (`--print-schema`), validate without sending (`--dry-run`), run.\n\nMBQL 5 bodies use numeric IDs (`database: 1`, `source-table: 7`) and POST to `/api/dataset`. The bundled query schema is synced from `@metabase/representations`; `id.yaml` is overridden to require positive integers for every ID `$def`.\n\n```sh\nmb query --print-schema                     # JSON Schema bundle\ncat q.json | mb query --dry-run             # validate, no network\nmb query --file q.json\nmb query --file q.json --skip-validate      # bypass pre-flight; let server reject\n```\n\nBody sources: `--file`, `--body`, or stdin (exactly one). Body is JSON.\n\nAny non-MBQL 5 body skips pre-flight automatically — legacy MBQL 4 (`{ \"type\": \"query\", \"database\": N, \"query\": { \"source-table\": T, ... } }`), legacy native (`{ \"type\": \"native\", \"database\": N, \"native\": { \"query\": \"...\" } }`), or any other shape that doesn't carry `\"lib/type\": \"mbql/query\"`. The bundled schema only models MBQL 5; `/api/dataset` normalizes the rest server-side via `lib-be/normalize-query` (the same normalizer that backs `card create` / `transform create`), so behavior is symmetric across endpoints. `--dry-run` on a non-MBQL 5 body emits `{ ok: true, errors: [] }` (no schema applies). The double-wrap footgun — an MBQL 5 query nested inside a `{type:\"query\", query:…}` envelope — is still rejected with a `ConfigError` before send.\n\n`--skip-validate` is an escape hatch when the bundled schema disagrees with what the server actually accepts (drift, false negative, edge case) for MBQL 5 bodies. Validation is skipped entirely and the body is sent as-is. Mutually exclusive with `--dry-run` (which is itself the validation mode).\n\nExit codes:\n\n- `0` — valid (and the query ran successfully when not in dry-run).\n- `2` — validation failed, malformed body, or `ConfigError`.\n- `1` — server-side error after a valid pre-flight (network, HTTP 4xx/5xx).\n\nOutput by mode:\n\n- `--print-schema` — `{ schema, defs: { \"id.yaml\", \"parameter.yaml\", \"ref.yaml\", \"temporal_bucketing.yaml\" } }`. The query schema's `$ref`s point into the `defs` namespace by file path; an agent can either feed the bundle directly into Ajv (`addSchema(defs[\"id.yaml\"], \"id.yaml\")` etc., then `compile(schema)`) or read it as documentation.\n- `--dry-run` — `{ ok: boolean, errors: { path: string, message: string }[] }`. `path` is a JSON Pointer into the body, `message` is the Ajv error string.\n- Run failure (no `--dry-run`) — same `{ ok, errors }` envelope on stdout, exit 2, no request made.\n- Run success — the streamed `CardQueryResult`.\n\n### MBQL 5 pre-flight in `card create`/`update`, `transform create`/`update`, `measure create`/`update`, and `segment create`/`update`\n\nWhen the embedded query (`card.dataset_query`, `transform.source.query` for `source.type: \"query\"`, or `measure.definition` / `segment.definition`) is MBQL 5 (`lib/type: \"mbql/query\"`), it is pre-flight-validated against the same schema as `mb query`. Validation failure: `{ ok, errors }` envelope on stdout, exit 2, request not made. MBQL 4 (legacy) bodies and Python transform sources skip validation — they're still accepted by the server and we don't ship a schema for them.\n\nPass `--skip-validate` to bypass the pre-flight on any of `card create`, `card update`, `transform create`, `transform update`, `measure create`, `measure update`, `segment create`, or `segment update` — the body is sent as-is and the server is the authority. Same escape hatch as on `mb query`; use only when the bundled schema disagrees with what the server actually accepts.\n\nAgent discovery path: `mb \u003ccommand\u003e --help --json` lists a command's args, JSON-body input schema, and output schema; the description for `card create`/`update`, `transform create`/`update`, `measure create`/`update`, and `segment create`/`update` references `mb query --print-schema` so an agent can fetch the validating schema directly.\n\nThe bundled query schema is synced from a pinned `@metabase/representations` release via `bun run sync:representations`; CI guards against drift.\n\n### Card-reference pre-flight in `dashboard create` / `dashboard update`\n\nBefore either command sends anything, every positive `card_id` referenced from the body's `dashcards` array is checked against `GET /api/card/:id` in parallel (de-duplicated per id). Cards that don't exist, are archived, or aren't readable fail pre-flight: the CLI writes a `{ ok: false, errors: [{ path, message }] }` envelope to stdout (one entry per offending dashcard, `path` is a JSON pointer like `/dashcards/3/card_id`) and exits **2** with `dashboard card-reference pre-flight failed: N error(s) — fix the dashcard card_id values listed above` on stderr. No dashboard is created or modified on a pre-flight miss — this is the contract that prevents orphan dashboards when a stale spec references an archived or missing card.\n\nThere is no `--skip-validate` escape hatch here. The pre-flight queries live server state (no bundled schema to drift from), so the only legitimate path on a pre-flight miss is to fix the input.\n\nIf the chained `PUT /api/dashboard/:id` fails _after_ the create has already inserted the row (rare with pre-flight in place, but possible on a permission / 5xx / network failure mid-flight), the user-facing error is rewritten to `dashboard \u003cid\u003e created but follow-up PUT /api/dashboard/\u003cid\u003e failed: \u003creason\u003e; dashcards not applied`, so the caller knows the orphan exists. Recovery: `dashboard update \u003cid\u003e --body '{\"dashcards\":[...]}'` to retry the dashcards, or `dashboard update \u003cid\u003e --body '{\"archived\":true}'` to archive the orphan.\n\n## UUIDs\n\n### `mb uuid`\n\nMint UUID v4 strings (Node `crypto.randomUUID`) for MBQL clause `lib/uuid` slots, native template-tag ids, and any other Metabase-side identifier whose schema enforces RFC 4122 format. Agents must call this command to obtain UUIDs rather than authoring them by hand: the bundled MBQL 5 schema rejects placeholder strings (`a1`, `uuid-1`, etc.) at `format: \"uuid\"` validation.\n\n```sh\nmb uuid                          # one UUID\nmb uuid --count 5                # five UUIDs, one per line (text mode in a TTY, JSON when piped)\nmb uuid --count 5 --json         # explicit JSON: [\"…\", \"…\", \"…\", \"…\", \"…\"]\nmb uuid --count 5 --format text  # explicit text: one UUID per line\n```\n\nOutput: text mode prints one UUID per line; JSON mode prints a `string[]`. Default behavior follows the standard `--format auto` rule — JSON when stdout is a pipe, text when it's a TTY.\n\n`--count` accepts integers `1` through `10000`; outside that range exits 2 with a `ConfigError`.\n\nExit codes: `0` success, `2` invalid `--count`.\n\n## Upgrade\n\n### `mb upgrade`\n\nSelf-update the CLI. Fetches the latest published version from the npm registry's `/-/package/\u003cpkg\u003e/dist-tags` endpoint, detects how the binary was installed (npm-global / npm-local / npx / dev / unknown — for the global case, also which package manager: npm, pnpm, yarn, or bun), and either runs the matching install command (for npm-style globals, after confirmation) or prints the exact command to run by hand.\n\n```sh\nmb upgrade                 # interactive: check + confirm + run for global installs\nmb upgrade --check         # print status only, never install\nmb upgrade --check --json  # structured plan for agents\nmb upgrade --yes           # skip the confirmation prompt\nmb upgrade --to 0.1.2      # pin a specific version (also valid for downgrades)\n```\n\nFlags:\n\n- `--check` — print the upgrade plan without installing.\n- `--yes` / `-y` — skip the confirmation prompt; only meaningful when the install method is auto-installable.\n- `--to \u003cversion\u003e` — target a specific semver instead of the registry `latest`. Useful for pinning or rolling back.\n- `--registry \u003curl\u003e` — override the npm registry (default `https://registry.npmjs.org`). The same URL the CLI hits to fetch dist-tags; the actual install always goes through your local `npm` / `pnpm` / `yarn` / `bun` which use their own configured registry.\n\nJSON output (UpgradeStatus):\n\n```json\n{\n  \"packageName\": \"@metabase/cli\",\n  \"currentVersion\": \"0.1.2\",\n  \"latestVersion\": \"0.1.3\",\n  \"targetVersion\": \"0.1.3\",\n  \"updateAvailable\": true,\n  \"changeRequired\": true,\n  \"installMethod\": \"npm-global\",\n  \"packageManager\": \"npm\",\n  \"binaryPath\": \"/usr/local/lib/node_modules/@metabase/cli/dist/cli.mjs\",\n  \"command\": {\n    \"argv\": [\"npm\", \"install\", \"-g\", \"@metabase/cli@0.1.3\"],\n    \"display\": \"npm install -g @metabase/cli@0.1.3\"\n  },\n  \"canAutoInstall\": true\n}\n```\n\nAuto-install happens only when `installMethod === \"npm-global\"`; everything else (local installs, npx, dev checkouts) prints the upgrade command and exits. In non-TTY runs without `--yes`, the command never prompts.\n\nExit codes: `0` success (including up-to-date / printed-instructions), `1` registry or install failure, `2` invalid `--to` value, `130` user cancelled the prompt.\n\n## Skills\n\nThe CLI ships with bundled agent skills (Claude Code / `npx skills add` compatible) that document `mb` itself. Content is served at runtime from the installed CLI version, so the instructions an agent fetches always match the binary it's about to run — no drift between a separately-installed skill copy and the CLI.\n\n```sh\nmb skills list                              # discover bundled skills (table or JSON)\nmb skills get core                          # print the top-level guide\nmb skills get core --full                   # include references and templates\nmb skills get git-sync,transform            # comma-separated, multi-skill fetch\nmb skills get --all --json --max-bytes 0    # every non-hidden skill, structured (default cap truncates)\nmb skills path                              # absolute paths for direct Read\nmb skills path core                         # one path\n```\n\n`mb skills get` honors the shared `--max-bytes` list cap. With the default 24 576 cap, `--all` will return only the first skill and emit a truncation notice — pass `--max-bytes 0` to dump every skill in one envelope.\n\nBundled skills:\n\n| Name            | Use                                                                                     |\n| --------------- | --------------------------------------------------------------------------------------- |\n| `core`          | Top-level guide: auth, flag conventions, output flags, body input, every command group  |\n| `data-workflow` | Front-door router for the whole journey (raw → clean tables → definitions → dashboards) |\n| `mbql`          | Authoring and fixing MBQL 5 query bodies                                                |\n| `native-sql`    | Native SQL query bodies: template tags, field filters, snippets, card references        |\n| `visualization` | Choosing a card's `display` and authoring `visualization_settings`                      |\n| `dashboard`     | Interactive dashboards: filter wiring, linked filters, cross-filtering, click behavior  |\n| `metadata`      | Semantic types, FK targets, dropdown behavior, and the features each unlocks            |\n| `transform`     | Authoring and running transforms (native SQL + MBQL 5), iteration, run inspection       |\n| `notification`  | Scheduled delivery: question alerts and dashboard subscriptions                         |\n| `document`      | Authoring document bodies: the TipTap JSON tree, embedding cards, entity links          |\n| `git-sync`      | Round-tripping Metabase content to/from a git remote                                    |\n\nDiscovery surfaces:\n\n- **Claude Code plugin marketplace**: `.claude-plugin/marketplace.json` declares a `metabase-cli` plugin pointing at the in-repo discovery stub. Users install with `/plugin marketplace add metabase/mb-cli` then `/plugin install metabase-cli@metabase`.\n- **`npx skills add`**: the same stub at `skills/metabase-cli/SKILL.md` is picked up by `npx skills add metabase/mb-cli`. The stub is intentionally minimal — it redirects the agent at `mb skills get core` so the real workflow content always comes from the installed CLI version.\n\nExit codes: `0` success, `2` `ConfigError` (missing name, unknown name, `MB_SKILLS_DIR` not a directory), `1` unexpected I/O.\n\n## Environment variables\n\n| Variable                 | Effect                                                                                                                                                                    |\n| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `MB_URL`                 | Default URL for `auth login` and config resolution.                                                                                                                       |\n| `MB_API_KEY`             | Default API key (makes `auth login` non-interactive, skipping the browser flow; not stored).                                                                              |\n| `MB_PROFILE`             | Default profile when `--profile` is omitted. Falls back to `default`.                                                                                                     |\n| `MB_VERBOSE`             | When set to `1`, prints structured developer-detail JSON to stderr on failure.                                                                                            |\n| `MB_CLI_SKIP_PREFLIGHT`  | When set to `1`, bypasses the per-command server version / token-feature preflight check. Escape hatch for patched Metabase builds; can mask real compatibility problems. |\n| `MB_CLI_DISABLE_KEYRING` | When set to `1`, skips the OS keychain and stores credentials as plaintext in the profiles file.                                                                          |\n| `MB_SKILLS_DIR`          | Override the directory `mb skills` scans (dev/test only; defaults to the CLI's bundled `skills` + `skill-data` trees).                                                    |\n\nThe former `METABASE_`-prefixed names (`METABASE_URL`, `METABASE_API_KEY`, `METABASE_PROFILE`, `METABASE_VERBOSE`, `METABASE_CLI_SKIP_PREFLIGHT`, `METABASE_CLI_DISABLE_KEYRING`) are deprecated but still honored; the CLI prints a one-line warning to stderr when it falls back to one. Switch to the `MB_`-prefixed names.\n\n## Agent integration\n\n### `--help --json`\n\nEvery node of the command tree answers `--help --json` with machine-readable help, mirroring what text help shows at that level:\n\n- A leaf command emits its full entry — name, description, `details`, examples, citty args with types/defaults/enums, `capabilities` (min server version / token feature), and the input and output Zod schemas rendered as JSON Schema (`inputSchema` is the exact validator `readBody` enforces on the JSON body, `null` for commands that take none).\n- A command group (and the root) emits a flat `{ commands: [{ command, description }] }` index of every leaf in its subtree, with full-path names.\n\n```sh\nmb --help --json | jq -r '.commands[].command'    # every command\nmb card query --help --json | jq .outputSchema    # one command's output schema\nmb card create --help --json | jq .inputSchema    # the JSON-body contract it validates\n```\n\nThe entry and index schemas (`CommandHelpEntry`, `CommandHelpIndex`) are exported from `src/runtime/command-help.ts`.\n\n## Exit codes\n\n| Code  | Meaning                                                |\n| ----- | ------------------------------------------------------ |\n| `0`   | Success.                                               |\n| `1`   | Verification or operation failed.                      |\n| `2`   | Configuration error (invalid flag, missing TTY, etc.). |\n| `130` | Interactive prompt cancelled (Ctrl+C).                 |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetabase%2Fmetabase-cli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmetabase%2Fmetabase-cli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetabase%2Fmetabase-cli/lists"}