Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3db9997
Add data actions to the client, CLI and skills
ranquild Oct 2, 2026
31ca6bb
Merge pull request #66 from metabase/query-actions
ranquild Oct 2, 2026
99fb3e5
Add transform-test support
ranquild Sep 16, 2026
544cd92
Close the transform-test bodies and document the shape
ranquild Sep 16, 2026
8dfbce1
Show both expectation types in the transform-test example
ranquild Sep 16, 2026
1f2fc29
Give the two example expectations different jobs
ranquild Sep 16, 2026
532981d
Rename database_type to cast_type
galdre Sep 17, 2026
ded9ef1
Check in an adapted form of DWT skill
galdre Sep 17, 2026
6151659
Let the transform skill handle the impl details, keep the test skill …
galdre Sep 17, 2026
3194dd2
Transform tests ship in v65
ranquild Sep 18, 2026
ac9f5e6
fix(cli): run transform-test commands on main's client
alxnddr Sep 22, 2026
ee280b2
fix(cli): match transform-test prose to Metabase master
alxnddr Sep 28, 2026
886357e
fix(cli): report a failed transform test as an outcome, bound its run…
alxnddr Sep 28, 2026
8fae38a
fix(client): name the failed-result error category failed and use it …
alxnddr Sep 28, 2026
1880d93
bump
alxnddr Oct 2, 2026
5663bfa
fix(cli): report a failed transform test's outcome when its report is…
alxnddr Oct 2, 2026
38edaba
fix(cli): spell transform-test list's filter --transform-id
alxnddr Oct 2, 2026
e428baf
docs(skills): ground the transform-test prose in the server and resto…
alxnddr Oct 2, 2026
5a254f7
docs(skills): keep core under the default output cap
alxnddr Oct 2, 2026
efb54aa
test: drop the skills e2e suite
alxnddr Oct 2, 2026
5cbe297
Merge pull request #59 from metabase/fix/transform-test-ee-path
alxnddr Oct 2, 2026
22f4d96
Merge origin/main into release-65-integration-branch
alxnddr Oct 7, 2026
98ca474
fix(client): explain a data action update the server rejects for its …
alxnddr Oct 7, 2026
2a77c19
fix
alxnddr Oct 8, 2026
4b082fa
ci
alxnddr Oct 8, 2026
3e4c2b3
Sync response and input enums with Metabase master
ranquild Oct 8, 2026
9ff6755
Follow Metabase head in the data action and Library e2e tests
ranquild Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 146 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,85 @@ mb transform-tag delete 5 --yes
| ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `--yes` | Skip the interactive confirmation prompt. In non-TTY contexts the prompt is skipped automatically (kubectl/gh/docker convention). |

## Transform tests

CRUD and run on `/api/ee/transform-test`. Requires Metabase v65 or newer with the `transforms-testing` premium feature. A transform test pins a transform's behaviour: every table the transform reads is replaced by an `input` fixture, the transform runs into a temp table, and each `expectation` checks that output. The temp tables are dropped when the run ends. The transform and the expectations read only those temp tables; a `format: "sql"` input runs verbatim against the transform's source database, so it may read real tables. Only query transforms (native SQL or MBQL) can be tested, on Postgres, MySQL, H2, ClickHouse, Redshift or SQL Server.

An input names its `table` and carries either `format: "sql"` with a `sql` query or `format: "rows"` with `columns` (each a `name` and a `cast_type` the warehouse accepts as a `CAST` target) and `rows`; every row carries exactly the declared columns.

An expectation is either `type: "empty"` with the `sql` that must return no rows, or `type: "equals"` with `format: "rows"` and the `columns` and `rows` the output must hold exactly. Expectation names are unique within a test. Expectation SQL may name only the transform's target table and its declared input tables, which are rewritten to the run's temp tables; any other table, or a column qualified by a table name rather than an alias, is refused with `transform-test.unremapped-reference`.

Create and update bodies are closed at every level, so a test read back with `get --full` has to shed `id`, `entity_id`, `creator_id`, `created_at` and `updated_at` before it can be sent back:

```sh
mb transform-test get 1 --full --json \
| jq 'del(.id, .entity_id, .creator_id, .created_at, .updated_at)' \
| mb transform-test update 1
```

### `mb transform-test list`

```sh
mb transform-test list --json
mb transform-test list --transform-id 1
```

| Flag | Description |
| --------------------- | ------------------------------------ |
| `--transform-id <id>` | Only the tests of this transform id. |

### `mb transform-test get <id>`

The compact form carries the id, transform, name and description; `--full` adds the `inputs` and `expectations` themselves.

```sh
mb transform-test get 1
mb transform-test get 1 --full --json
```

### `mb transform-test create`

```sh
mb transform-test create --file transform-test.json
```

| Flag | Description |
| --------------- | ----------------------- |
| `--body <json>` | Inline JSON body. |
| `--file <path>` | Path to JSON body file. |

### `mb transform-test update <id>`

Only the fields the body carries are patched; `inputs` and `expectations` replace what is stored rather than merging into it.

```sh
mb transform-test update 1 --body '{"name":"renamed"}'
```

### `mb transform-test delete <id>`

```sh
mb transform-test delete 1 --yes
```

| Flag | Description |
| ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `--yes` | Skip the interactive confirmation prompt. In non-TTY contexts the prompt is skipped automatically (kubectl/gh/docker convention). |

### `mb transform-test run <id>`

Runs the transform against the fixtures and reports what each expectation found. Exits 1 when the test does not pass, so it drops straight into CI.

```sh
mb transform-test run 1
mb transform-test run 1 --json
mb transform-test run 1 --timeout 300000
```

| Flag | Description |
| ---------------- | -------------------------------------------------------------------------- |
| `--timeout <ms>` | Request timeout in ms (default 30000); the run is one synchronous request. |

## Databases

Read 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.
Expand Down Expand Up @@ -1058,6 +1137,72 @@ mb snippet archive 1
mb snippet archive 1 --json
```

## Data actions

CRUD on `/api/action` plus `execute`. A data action is a parameterized native SQL write (`INSERT`, `UPDATE`, `DELETE`) filed in a collection. Data actions are off by default: an admin must enable them on the target database first. `mb data-action create` authors data actions only — no `model_id`, no implicit data actions — and needs a server whose data actions do not require a model.

### `mb data-action list`

```sh
mb data-action list
mb data-action list --json
```

### `mb data-action get <id>`

```sh
mb data-action get 1
mb data-action get 1 --json --full
```

### `mb data-action create`

```sh
cat data-action.json | mb data-action create
mb data-action create --file data-action.json
mb data-action create --file data-action.json --skip-validate
```

| Flag | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--body <json>` | Inline JSON body. |
| `--file <path>` | Path to JSON body file. |
| `--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. |

Body fields: `name` (required), `type` (required, `"query"`), `database_id` (required), `dataset_query` (required native query whose `{{tag}}` placeholders are its inputs), `parameters` (one per template tag), `collection_id` (optional; the root when omitted), `description`, `visualization_settings`.

### `mb data-action update <id>`

Patch a data action; the body carries only the fields to change (`name`, `database_id`, `dataset_query`, `parameters`, `collection_id`, `description`, `visualization_settings`, `archived`).

```sh
cat patch.json | mb data-action update 1
mb data-action update 1 --body '{"name":"Rename an order"}'
```

### `mb data-action archive <id>`

```sh
mb data-action archive 1
```

### `mb data-action delete <id>`

Delete a data action and the dashboard buttons that run it.

```sh
mb data-action delete 1 --yes
```

### `mb data-action execute <id>`

Run a data action. The body is `{"parameters": {...}}` keyed by parameter id; the result reports `rows-affected`.

```sh
mb data-action execute 1 --body '{"parameters":{"id":1,"note":"rush"}}'
mb data-action execute 1 --file values.json
```

## Segments

CRUD 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.
Expand Down Expand Up @@ -1925,7 +2070,7 @@ mb search revenue --search-native-query --include-metadata --full --json

| Flag | Description |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--models`, `-m` | Comma-separated model filter: `card,dataset,metric,dashboard,collection,database,table,segment,measure,document,action,transform,indexed-entity`. |
| `--models`, `-m` | Comma-separated model filter: `card,dataset,metric,dashboard,collection,database,table,segment,measure,document,exploration,action,transform,indexed-entity`. |
| `--archived` | Include archived items only. |
| `--limit` | Max results to return (default `20` — `search` is the one list verb with its own default). |
| `--offset` | Where the window starts, applied by the server (default `0`). |
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metabase/cli",
"version": "0.5.0",
"version": "0.5.1",
"description": "Metabase CLI",
"license": "AGPL-3.0",
"repository": {
Expand Down
16 changes: 11 additions & 5 deletions packages/cli/skill-data/core/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Top-level command groups (run `mb <group> --help` to discover verbs):

```
auth | db | table | field | upload | content-translation | query | card | dashboard | snippet | segment | measure | collection | library
document | glossary | timeline | timeline-event | transform | transform-job | transform-tag | alert | subscription | setting
search | dependency | git-sync | setup | eid | uuid | upgrade | skills
document | glossary | timeline | timeline-event | transform | transform-job | transform-tag | transform-test | alert | subscription | setting
search | dependency | git-sync | data-action | setup | eid | uuid | upgrade | skills
```

The conventions below — auth, flags, output, body input — hold across **every** group. Per-command flags and examples live in each command's `--help`; add `--json` for the machine-readable form with the output JSON Schema. A few flows have their own skills (see "Specialized skills"). When a card needs a query, prefer MBQL over native SQL (portable, pre-flight-validated — load `mbql`); fall back to native SQL when MBQL can't express it.
Expand Down Expand Up @@ -124,7 +124,7 @@ mb transform --help --json | jq -r '.commands[].command' # verbs under "transfo

## Resource quirks worth memorizing

Routine verb shapes (list / get / create / update), every flag, and output schemas live in each command's `--help` (add `--json` for output schemas). Below is only what help does _not_ tell you: footguns and non-obvious behaviors.
What `--help` doesn't say: footguns and non-obvious behaviors.

- **db traversal: the hydration ladder.** Start with `database get <db-id> --include tables` — the compact table map (id, name, schema, description per table), one call that fits most databases. Pick the relevant tables, then `table fields <table-id>` per table (bounded: fields are per-table). `--include tables.fields` is the full rollup — small databases only. Hundreds of tables? Traverse by schema (`database schemas <db-id>` → `database schema-tables <db-id> <schema>`) or look tables up by name (`search <term> --models table --db-id <db-id> --limit 10`). `db sync-schema` / `rescan-values` queue async work and return `{status:"ok"}` at once; `sync-schema --wait` blocks until `initial_sync_status: complete`.
- **table fields.** `table get` never returns fields on its own — pass `--include fields` (compact; the underlying query_metadata response also carries FK targets and dimensions, visible under `--full`) or use `table fields <id>` (list envelope). `table update` patches table-level metadata only; physical columns aren't editable.
Expand All @@ -134,7 +134,7 @@ Routine verb shapes (list / get / create / update), every flag, and output schem
- **content-translation.** Admin-only, and separate from Remote Sync. `content-translation download > translations.csv` streams the complete active dictionary; `content-translation upload --file translations.csv` replaces every active translation with the file's contents. Always upload the canonical complete CSV, never a partial patch. An empty dictionary downloads as Metabase's four-row sample dictionary — don't re-upload it as real translations. Metabase limits dictionaries to 1.5 MiB.
<!-- /requires -->
- **card.** `dataset_query` is the `mbql/query` object itself (→ `mbql`). `--export-format csv|xlsx` streams the raw export (pipe to a file), bypassing the JSON envelope. `archive` is the only delete; unarchive with `update --body '{"archived":false}'`. `visualization_settings` keys are scoped by `display` and aren't pre-flighted — see `visualization`.
- **dashboard.** Dashcards round-trip through `PUT /api/dashboard/:id` (no per-dashcard endpoint): `update-dashcard <dash-id> <dashcard-id>` patches one safely; `update --body '{"dashcards":[…]}'` replaces the whole set (omitted ids are deleted server-side; negative ids for new cards). Every dashcard must include `card_id`, including existing rows; use `card_id:null` plus a `visualization_settings.virtual_card` block (`{display:"text"|"heading"|"link"|…}`) for non-question cards. `create` accepts the **same** `dashcards` array in its initial body, so lay out the whole dashboard in one call. `create`/`update` pre-flight every positive `card_id` and exit **2** with `{ok:false,errors:[…]}` on a bad ref (non-bypassable). `dashboard get <id>` (or `--full`) hydrates dashcards/tabs; `list` omits them. **The grid is 24 columns wide:** each dashcard's `{col, row, size_x, size_y}` is in grid units — **full-width is `size_x: 24`**. Keep `col + size_x ≤ 24`, start a full-width stack's `col` at 0, and don't overlap (the server stores collisions as sent — no auto-fix). Layout patterns and per-chart default sizes → the `dashboard` skill; load it before composing any `dashcards` array.
- **dashboard.** Dashcards round-trip through `PUT /api/dashboard/:id` (no per-dashcard endpoint): `update-dashcard <dash-id> <dashcard-id>` patches one safely; `update --body '{"dashcards":[…]}'` replaces the whole set (omitted ids are deleted server-side; negative ids for new cards). Every dashcard, existing ones included, needs `card_id`; use `card_id:null` plus a `visualization_settings.virtual_card` block (`{display:"text"|"heading"|"link"|…}`) for non-question cards. `create` takes the **same** `dashcards` array, so lay out the whole dashboard in one call. `create`/`update` pre-flight every positive `card_id` and exit **2** with `{ok:false,errors:[…]}` on a bad ref (non-bypassable). `dashboard get <id>` (or `--full`) hydrates dashcards/tabs; `list` omits them. **The grid is 24 columns wide:** each dashcard's `{col, row, size_x, size_y}` is in grid units — **full-width is `size_x: 24`**. Keep `col + size_x ≤ 24`, start a full-width stack's `col` at 0, and don't overlap (the server stores collisions as sent). Load the `dashboard` skill (layout patterns, per-chart default sizes) before composing any `dashcards` array.
- **dashboard parameters (filters).** A dashboard's `parameters` array holds its filter widgets; they're part of the dashboard record, so read them with `dashboard get <id> --fields parameters --json` (no separate verb). **Editing replaces the _whole_ array** (like dashcards), so it's a read-modify-write loop and omitting a parameter deletes it. A parameter only filters a card once it is **mapped** onto that dashcard's `parameter_mappings` — an unmapped parameter is an inert widget. `type` is a **closed enum**; an unlisted value is a hard parse error that echoes the full allowed set back to you. `dashboard parameter-values <id> <parameter-id> [--query <substr>]` fetches a widget's selectable values (`{values, has_more_values}`; `--query` is a case-insensitive substring search). Parameter types, ids, mapping targets, and value sources → the `dashboard` skill; load it before authoring a `parameters` array.
<!-- requires: contentVerification -->
- **verify.** `card verify <id>` / `dashboard verify <id>` (admins only) mark it verified (rank boost, `search --verified`). `--remove` (or a card query edit) withdraws it; `--text` adds a note; `get <id> --fields moderation_reviews` reads it. Verify only what you checked and the user approved.
Expand Down Expand Up @@ -174,9 +174,15 @@ This file is enough for any single-command task. For anything deeper, load the r
- **`notification`** — scheduled delivery: question alerts (`mb alert`) and dashboard subscriptions (`mb subscription`). Choosing between them, the two schedule/recipient contracts, channel prerequisites, testing a send.
<!-- requires: transforms -->
- **`transform`** — transform body JSON, create + run-with-wait, run inspection, tags, jobs.
<!-- /requires -->
<!-- requires: transformTests -->
- **`transform-test-plan`** — planning transform tests.
<!-- /requires -->
- **`document`** — Metabase documents (TipTap body, embedding cards).
<!-- requires: remoteSync -->
<!-- requires: dataActionsWithoutModel -->
- **`data-action`** — saved SQL writes: enabling, body, parameters, running.
<!-- /requires -->
<!-- requires: remoteSync -->
- **`git-sync`** — round-tripping content to/from a git remote.
<!-- /requires -->
- **`data-workflow`** — the guided, end-to-end data workflow: investigate raw data, build clean analysis-ready tables, define reusable segments/measures/metrics, answer questions, build dashboards. **Start here when the user states a goal rather than a single verb** — "make sense of my data", "build a data model", "be my data analyst". It detects where the data is and routes to the right stage.
Expand Down
Loading
Loading