@metabase/cli 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/README.md +147 -101
- package/dist/{add-collection-CffaBB-Y.mjs → add-collection-BU8r3r2M.mjs} +9 -4
- package/dist/add-collection-C0w6ACQF.mjs +11 -0
- package/dist/{archive-uJrslh9r.mjs → archive-BNinrUak.mjs} +9 -8
- package/dist/{archive-GdGm7l2e.mjs → archive-C1enZgKV.mjs} +8 -7
- package/dist/archive-CDA0KxL8.mjs +40 -0
- package/dist/{archive-BAcEXbT9.mjs → archive-CRhiBpPJ.mjs} +9 -8
- package/dist/{archive-B_B3MQp0.mjs → archive-DMPS8Kih.mjs} +9 -8
- package/dist/archive-lWgqiFAt.mjs +40 -0
- package/dist/auth-CzXb_zB2.mjs +19 -0
- package/dist/{body-D6dHGjMT.mjs → body-DjdFxjpg.mjs} +4 -4
- package/dist/{branches-Bpe40fEd.mjs → branches-B1WRfG7-.mjs} +11 -7
- package/dist/{cancel-BWTY6oYI.mjs → cancel-Dl_Ho056.mjs} +7 -6
- package/dist/{cancel-task--BfiAXfS.mjs → cancel-task-CdigdCaO.mjs} +11 -7
- package/dist/capabilities-7e9MgquN.mjs +29 -0
- package/dist/card-DP4rfoOi.mjs +21 -0
- package/dist/{card-CQxvHeyP.mjs → card-DlCAaAPq.mjs} +1 -1
- package/dist/{cards-CVlFJxYh.mjs → cards-BGiJS675.mjs} +8 -7
- package/dist/cli.mjs +267 -44
- package/dist/collection-tY18ezvn.mjs +21 -0
- package/dist/{predicates-CGO17Q15.mjs → command-augment-BH9qgQ5u.mjs} +66 -14
- package/dist/create-BNiva__H.mjs +52 -0
- package/dist/{create-izE3EKCt.mjs → create-BTcpaop_.mjs} +9 -8
- package/dist/{create-BykvNpSA.mjs → create-BYlIju0b.mjs} +14 -12
- package/dist/{create-Bu-YhIDL.mjs → create-Be_0Vier.mjs} +10 -9
- package/dist/{create-DYoc9IXW.mjs → create-CHF313Qg.mjs} +13 -9
- package/dist/{create-Cz3_Wxdt.mjs → create-CwGtmwqm.mjs} +14 -12
- package/dist/{create-DP8RrLDi.mjs → create-CzzrbL0u.mjs} +10 -9
- package/dist/{create-BzElku2l.mjs → create-DGth_uOp.mjs} +14 -12
- package/dist/{create-branch-B49UQyCK.mjs → create-branch-DKZkoQ64.mjs} +11 -7
- package/dist/{create-DQVdMT2Y.mjs → create-dhxPxfF3.mjs} +16 -14
- package/dist/{credentials-xKSoP6eh.mjs → credentials-dzeq7ckm.mjs} +12 -10
- package/dist/{current-task-DweHmjlk.mjs → current-task-CCRzm0_7.mjs} +11 -7
- package/dist/dashboard-ChM_Tu0l.mjs +22 -0
- package/dist/{dashboard-CnMD04PQ.mjs → dashboard-FY5UzJ_Z.mjs} +2 -1
- package/dist/{database-BNlvldUL.mjs → database-CIXwHKjK.mjs} +3 -3
- package/dist/{database-vvig8k4x.mjs → database-lH-B3G1I.mjs} +1 -1
- package/dist/db-DrQn_i3W.mjs +22 -0
- package/dist/{remove-B3ZEqBF7.mjs → delete-CM3jnAeQ.mjs} +21 -20
- package/dist/{delete-DojHmKeM.mjs → delete-Dimc-2y8.mjs} +9 -8
- package/dist/{delete-DIz9Tgz5.mjs → delete-ZjnV35OJ.mjs} +9 -8
- package/dist/{delete-runtime-BkAdygbs.mjs → delete-runtime-B6RQo_pw.mjs} +5 -3
- package/dist/{delete-table-DjN8E3sd.mjs → delete-table-agZJpivt.mjs} +9 -8
- package/dist/{deprovision-_HDcBApz.mjs → deprovision-CwxcIT3k.mjs} +16 -12
- package/dist/{dirty-Co8V0SZ3.mjs → dirty-D4d0yHqj.mjs} +11 -7
- package/dist/{docker-D9sC_37H.mjs → docker-Oq80q3tu.mjs} +4 -4
- package/dist/{translate-CG_Ka0dO.mjs → eid-BXzaQh0o.mjs} +37 -22
- package/dist/error-C9S6PN3-.mjs +190 -0
- package/dist/{export-CVMFxoo1.mjs → export-DTygoXBP.mjs} +17 -16
- package/dist/field-Z6Pcxf4n.mjs +19 -0
- package/dist/{fields-Coha7vKv.mjs → fields-CoQi99gv.mjs} +9 -8
- package/dist/{get-DXv2FkA7.mjs → get-Bzys7vgp.mjs} +8 -7
- package/dist/{get-bNtA7vWe.mjs → get-C2p383Qc.mjs} +8 -7
- package/dist/{get-Br6WayZv.mjs → get-C3HdQ91a.mjs} +8 -7
- package/dist/{get-BOtKerj8.mjs → get-CP3Z3NiH.mjs} +9 -8
- package/dist/{get-BSKoL8ek.mjs → get-C_w1kvN3.mjs} +9 -8
- package/dist/{get-Be6EFh94.mjs → get-CzuzeKSe.mjs} +10 -9
- package/dist/{get-BVTz9B_H.mjs → get-D3SbEQSE.mjs} +10 -9
- package/dist/get-DFxZXaKz.mjs +79 -0
- package/dist/{get-DZrV7v9d.mjs → get-DQTZG_NP.mjs} +8 -7
- package/dist/{get-CJwzbVjc.mjs → get-DSWFjy7O.mjs} +8 -7
- package/dist/{get-BxzCKVC6.mjs → get-Ddr0XLh7.mjs} +8 -7
- package/dist/{get-AOvWo48B.mjs → get-Hc93A0Yz.mjs} +8 -7
- package/dist/{get-C_6K7MSW.mjs → get-lb7q3JYs.mjs} +7 -6
- package/dist/get-run-B7sKdaDU.mjs +38 -0
- package/dist/git-sync-CiGAad76.mjs +28 -0
- package/dist/{has-remote-changes-D6xgsuUr.mjs → has-remote-changes-BY10-nnE.mjs} +11 -7
- package/dist/{import-Dv0ORSNw.mjs → import-CiMz4Wz-.mjs} +17 -16
- package/dist/{input-BQ-BZA8h.mjs → input-cMSEqISy.mjs} +7 -4
- package/dist/{is-dirty-WNi8a6O9.mjs → is-dirty-BZOaryxT.mjs} +9 -4
- package/dist/is-dirty-Ume4oV0j.mjs +10 -0
- package/dist/{items-CTcAMknV.mjs → items-BWfvkY-J.mjs} +9 -8
- package/dist/key-C2XG394c.mjs +17 -0
- package/dist/license-Dxarh-gG.mjs +17 -0
- package/dist/{list-FXuSCYpa.mjs → list--OYdUTtu.mjs} +7 -6
- package/dist/{list-8oVMvlLV.mjs → list-2j7GsXsl.mjs} +7 -6
- package/dist/{list-xQmtQPSl.mjs → list-BI4zr8LW.mjs} +10 -8
- package/dist/{list-DhWG5jiW.mjs → list-Brgh-Z2v.mjs} +8 -6
- package/dist/{list-DSs0Q78i.mjs → list-C3hfovHv.mjs} +7 -6
- package/dist/{list-DvUjMQze.mjs → list-CL7eCOQE.mjs} +7 -6
- package/dist/list-Clz5igWg.mjs +44 -0
- package/dist/list-D4sFiqX8.mjs +173 -0
- package/dist/{list-BxdXvGTK.mjs → list-DXH7TlkU.mjs} +9 -7
- package/dist/{list-CocYwmnI.mjs → list-DZ8fNUoQ.mjs} +9 -8
- package/dist/{list-DjhZU-FY.mjs → list-SOG0whQ-.mjs} +7 -6
- package/dist/{list-DI7K3K6k.mjs → list-d58BprgJ.mjs} +7 -6
- package/dist/{list-NiwCL_1X.mjs → list-sD5N3fGk.mjs} +9 -8
- package/dist/{list-CbJeP0Z6.mjs → list-zSO0DMw-.mjs} +10 -6
- package/dist/{login-SXsSH0I1.mjs → login-Bm2AnCez.mjs} +65 -80
- package/dist/{logout-bgOXjxbN.mjs → logout-BlyRJODO.mjs} +8 -7
- package/dist/{logs-BnwVbFuD.mjs → logs-CywPikkL.mjs} +9 -8
- package/dist/{manifest-CGM7XNLC.mjs → manifest-BBR46KFM.mjs} +15 -15
- package/dist/measure-C44EK_xt.mjs +20 -0
- package/dist/{measure-BEQfnLdN.mjs → measure-ClESGxIb.mjs} +2 -2
- package/dist/{metadata-Bu2HOmuX.mjs → metadata-B8ZSF9LA.mjs} +10 -9
- package/dist/{metadata-B0WZT3Yb.mjs → metadata-DqiI2q9q.mjs} +9 -8
- package/dist/parse-enum-CrEWOhuY.mjs +11 -0
- package/dist/{parse-id-B3B-0hUA.mjs → parse-id-lk_K-CEF.mjs} +1 -1
- package/dist/{parse-ref-D1yeDOn8.mjs → parse-ref-BiETXmvm.mjs} +1 -1
- package/dist/{parse-schemas-DgtVLikM.mjs → parse-schemas-BqUdWUwq.mjs} +2 -2
- package/dist/path-AEtZ3mBq.mjs +58 -0
- package/dist/{poll-BCnrcUVf.mjs → poll-DHKDpCiq.mjs} +2 -2
- package/dist/{poll-task-0b1V6G-8.mjs → poll-task-Cooi0lQV.mjs} +3 -20
- package/dist/{preflight-5ACaYnDp.mjs → preflight-aXV5LyDs.mjs} +4 -4
- package/dist/{process-FjsqDwKo.mjs → process-C7V8LJ-j.mjs} +1 -1
- package/dist/{prompt-DgDNy_Pc.mjs → prompt-CFKoys7k.mjs} +3 -1
- package/dist/{provision-29Zt62Ft.mjs → provision-UWcNDoDe.mjs} +29 -24
- package/dist/{ps-BMFiRCi4.mjs → ps-CJU0EbrC.mjs} +5 -3
- package/dist/ps-DEroLgbI.mjs +11 -0
- package/dist/{query-DxA353Hy.mjs → query-AaKzYnTY.mjs} +9 -8
- package/dist/{query-aba8MEe_.mjs → query-BlsVNZpD.mjs} +15 -13
- package/dist/{remove-BfgU_CQi.mjs → remove-BFWun0e8.mjs} +9 -8
- package/dist/{remove-collection-Brv72xUe.mjs → remove-collection-CoCmrrQs.mjs} +13 -9
- package/dist/{render-DuoDUTVL.mjs → render-CfznwleY.mjs} +15 -17
- package/dist/render-OQn3iRsI.mjs +32 -0
- package/dist/{rescan-values-DIAdjoq7.mjs → rescan-values-C0FDsjT7.mjs} +10 -9
- package/dist/{run-CgXRo0hD.mjs → run-B4Wn43zm.mjs} +10 -9
- package/dist/{runs-DtLRw6xg.mjs → runs-Bbaszr18.mjs} +9 -8
- package/dist/{runtime-Br8L4NPm.mjs → runtime-Dmv5VtUK.mjs} +657 -428
- package/dist/{schema-tables-DiKMY6lx.mjs → schema-tables-CaWinbuK.mjs} +9 -8
- package/dist/{schemas-Bvr8cOzo.mjs → schemas-DUgGpAyB.mjs} +7 -6
- package/dist/{search-BT_TCcTd.mjs → search-BLrBXLUk.mjs} +12 -16
- package/dist/segment-B3Uwwcsm.mjs +20 -0
- package/dist/{set-DtG0KH6P.mjs → set-B8cUbRLD.mjs} +13 -12
- package/dist/{set-CAIkXlPy.mjs → set-DfGsta5O.mjs} +11 -10
- package/dist/{setting-BDOi5fk_.mjs → setting-D2p2MA7f.mjs} +3 -3
- package/dist/{setup-LjTvvlJy.mjs → setup-C9ikBRw_.mjs} +9 -8
- package/dist/skills-CUHIcQS6.mjs +18 -0
- package/dist/skills-CiN1OQ8W.mjs +191 -0
- package/dist/snippet-B7D0uWlz.mjs +20 -0
- package/dist/{start-CXKt0Q7A.mjs → start-3PX3ahjT.mjs} +68 -36
- package/dist/{stash-dRw1UEwg.mjs → stash-EIDcSvpF.mjs} +17 -16
- package/dist/{status-C2niMfrQ.mjs → status-95ElRAu9.mjs} +12 -8
- package/dist/status-B0_MiZEf.mjs +100 -0
- package/dist/status-CEplmC44.mjs +34 -0
- package/dist/{stop-BdedYfwU.mjs → stop-CQ0XGrN8.mjs} +11 -10
- package/dist/{summary-BPDA4K99.mjs → summary-C12LiEuJ.mjs} +8 -7
- package/dist/{sync-schema-D95LLRpf.mjs → sync-schema-Ba8M3DiX.mjs} +10 -9
- package/dist/{table-B-PYcgGb.mjs → table-C7a5V6Zn.mjs} +1 -1
- package/dist/table-e6h8SLVX.mjs +20 -0
- package/dist/transform-BMYh1lsC.mjs +25 -0
- package/dist/transform-job-Cm7z5TfH.mjs +20 -0
- package/dist/{transform-job-Csr86muI.mjs → transform-job-DeTDPMxt.mjs} +1 -1
- package/dist/{tree-DazZT7dR.mjs → tree-Des2ZG9d.mjs} +6 -5
- package/dist/{update-DE6kjV-f.mjs → update-Bx54nWEI.mjs} +17 -15
- package/dist/{update-bW-i6gjZ.mjs → update-CyIZdbIQ.mjs} +11 -10
- package/dist/{update-djgvzO3K.mjs → update-DBi5U8zb.mjs} +16 -14
- package/dist/{update-CJSDB6S8.mjs → update-DHZubok3.mjs} +18 -14
- package/dist/{update-BBfvArCx.mjs → update-DSgceARZ.mjs} +11 -10
- package/dist/{update-DSWZSfpw.mjs → update-DzAN4SPj.mjs} +15 -13
- package/dist/{update-WyRKlQPh.mjs → update-F6DmZncY.mjs} +11 -10
- package/dist/{update-DTIWJxob.mjs → update-_QfgNa53.mjs} +12 -11
- package/dist/{update-dashcard-BhD5x__K.mjs → update-dashcard-wpSjv4M7.mjs} +11 -10
- package/dist/{update-9kVyE3BJ.mjs → update-mYVnoYNV.mjs} +15 -13
- package/dist/{update-659eQR1L.mjs → update-njHe3j-s.mjs} +15 -13
- package/dist/{upgrade-D58rvXHM.mjs → upgrade-iAuvhX-W.mjs} +9 -8
- package/dist/{url-DKkSu2D8.mjs → url-DWaT6WIZ.mjs} +11 -10
- package/dist/{uuid-BF20B59s.mjs → uuid-CMKnS8-z.mjs} +8 -6
- package/dist/{validate-CB0bu50i.mjs → validate-dPEOnOf8.mjs} +2 -1
- package/dist/{validate-query-CavIA0Q2.mjs → validate-query-Cw6WE5Y8.mjs} +3 -3
- package/dist/{values-DyjmpcbT.mjs → values-BfSTAbzc.mjs} +8 -7
- package/dist/verify-D5YtTqqp.mjs +79 -0
- package/dist/{wait-CeUPCgdc.mjs → wait-8yV9_WIo.mjs} +2 -2
- package/dist/{wait-DhkTaV6E.mjs → wait-Bv3Tsnv4.mjs} +12 -8
- package/dist/{wait-flags-BR-yqe7y.mjs → wait-flags-Dzq9BGQY.mjs} +20 -9
- package/dist/workspace-CKLZrR7l.mjs +26 -0
- package/dist/{workspace-credentials-Cctumbru.mjs → workspace-credentials-BXpABsNZ.mjs} +2 -41
- package/dist/yaml-YTQiYJ9s.mjs +43 -0
- package/package.json +6 -2
- package/skill-data/core/SKILL.md +177 -0
- package/skill-data/git-sync/SKILL.md +196 -0
- package/skill-data/mbql/SKILL.md +156 -0
- package/skill-data/mbql/references/operators.md +253 -0
- package/skill-data/transform/SKILL.md +197 -0
- package/skill-data/viz/SKILL.md +137 -0
- package/skill-data/viz/references/settings.md +312 -0
- package/skill-data/workspace/SKILL.md +390 -0
- package/skills/metabase-cli/SKILL.md +21 -0
- package/dist/add-collection-CPL1njYZ.mjs +0 -11
- package/dist/api-key-9p1UPnXn.mjs +0 -13
- package/dist/auth-N4w5xtwW.mjs +0 -19
- package/dist/card-4rZRb5bc.mjs +0 -20
- package/dist/collection-Cp_B02I4.mjs +0 -19
- package/dist/command-augment-D9pI9Vbh.mjs +0 -11
- package/dist/create-doyv3SxU.mjs +0 -50
- package/dist/create-ov-De5dO.mjs +0 -125
- package/dist/dashboard-BYBiA-IG.mjs +0 -20
- package/dist/db-CObVU22j.mjs +0 -22
- package/dist/eid-Cr5r-t9B.mjs +0 -13
- package/dist/field-CbljasCH.mjs +0 -18
- package/dist/flag-pair-Fmcdkrfx.mjs +0 -17
- package/dist/get-run-CSrXHDGS.mjs +0 -36
- package/dist/git-sync-BGkS8o5b.mjs +0 -28
- package/dist/is-dirty-BOZ4xz92.mjs +0 -10
- package/dist/key-CCJdVWKc.mjs +0 -12
- package/dist/license-DLLTpFvP.mjs +0 -17
- package/dist/list-BNzdnE1c.mjs +0 -55
- package/dist/measure-B54VtKym.mjs +0 -19
- package/dist/package-D-aVYFKM.mjs +0 -80
- package/dist/ps-C5FOLwL2.mjs +0 -11
- package/dist/segment-C2ui5dSd.mjs +0 -19
- package/dist/snippet-BcgVYsoR.mjs +0 -19
- package/dist/status-BEONmJWv.mjs +0 -32
- package/dist/status-BWep0PFe.mjs +0 -56
- package/dist/table-lCNGbvej.mjs +0 -19
- package/dist/transform-BGAm1s4f.mjs +0 -24
- package/dist/transform-job-cNTJ30pm.mjs +0 -19
- package/dist/workspace-DtcBldk0.mjs +0 -24
- /package/dist/{body-flags-BK7J6Daz.mjs → body-flags-D7q87Btw.mjs} +0 -0
- /package/dist/{field-B3gvaqpK.mjs → field-yomXlkvl.mjs} +0 -0
- /package/dist/{paginate-CTSfuYiF.mjs → paginate-Dfm9eO9A.mjs} +0 -0
- /package/dist/{revision-message-flag-oyq2xrDU.mjs → revision-message-flag-WmsIzUOM.mjs} +0 -0
- /package/dist/{segment-BMrUBz94.mjs → segment-Be2v4ilr.mjs} +0 -0
- /package/dist/{setting-CTaAeMci.mjs → setting-oL97SNeO.mjs} +0 -0
- /package/dist/{snippet-Dw0Sjzkr.mjs → snippet-COggaWxx.mjs} +0 -0
- /package/dist/{transform-IEX4Mx3X.mjs → transform-GTW3G-01.mjs} +0 -0
- /package/dist/{workspace-C5q4nbpY.mjs → workspace-BBXJczJK.mjs} +0 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: transform
|
|
3
|
+
description: Author and run Metabase transforms via `mb` — body shape (native SQL + MBQL 5), create + run-with-wait, run inspection, cancel, the `update`-vs-recreate iteration rule, and the writable-keys-only PATCH contract. Load when the user touches transforms — "create a transform", "run a transform", "fix a failing transform", "list transform runs", "cancel a running transform", or anything `mb transform …`.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Transforms
|
|
8
|
+
|
|
9
|
+
A **transform** persists the result of a query (native SQL or MBQL) to a warehouse table the user can read from cards, dashboards, and other transforms. It runs on a schedule (via `transform-job`) or on-demand (`transform run`).
|
|
10
|
+
|
|
11
|
+
This skill covers the create-and-run flow. The general flag conventions, body-input precedence, and output flags live in the `core` skill (`mb skills get core`). If you're authoring a transform inside a workspace, also load the `workspace` skill for the canonical-vs-isolation-schema rule.
|
|
12
|
+
|
|
13
|
+
## Body shape
|
|
14
|
+
|
|
15
|
+
A transform has two halves:
|
|
16
|
+
|
|
17
|
+
- `source` — the query to run (`type: "query"`, with `query.type` of `native` or `mbql`).
|
|
18
|
+
- `target` — the warehouse destination (`type: "table"`, with `database`, `schema`, `name`).
|
|
19
|
+
|
|
20
|
+
Native SQL is the simplest source and the easiest to author by hand (see "Create + run" below). MBQL is what the Metabase UI emits and is more verbose; pull a sample with `mb transform get <id> --full --json` if you need its shape.
|
|
21
|
+
|
|
22
|
+
For an **MBQL 5** `source.query` (`lib/type: "mbql/query"`), the body shape, the "options object is always second" clause rule, UUID minting, aggregation/order-by refs, naming aggregation output columns, and the `--print-schema` → `--dry-run` validation loop are all in the `mbql` skill — **`mb skills get mbql`**. The MBQL-5 pre-flight on `transform create`/`update` is documented there too (legacy MBQL 4 and native sources skip it). For a transform target, naming your aggregation output columns matters more than usual — a bare `count` / `avg_2` becomes the warehouse column name; see the `mbql` skill's "Naming aggregation output columns".
|
|
23
|
+
|
|
24
|
+
## Create + run (native SQL)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cat > /tmp/transform.json <<'EOF'
|
|
28
|
+
{
|
|
29
|
+
"name": "user_counts_by_signup_year",
|
|
30
|
+
"description": "Sample transform: counts users by year of signup",
|
|
31
|
+
"source": {
|
|
32
|
+
"type": "query",
|
|
33
|
+
"query": {
|
|
34
|
+
"type": "native",
|
|
35
|
+
"database": <db-id>,
|
|
36
|
+
"native": {
|
|
37
|
+
"query": "SELECT date_trunc('year', created_at)::date AS signup_year, COUNT(*)::int AS user_count FROM public.users GROUP BY 1 ORDER BY 1"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"target": {
|
|
42
|
+
"type": "table",
|
|
43
|
+
"database": <db-id>,
|
|
44
|
+
"schema": "public",
|
|
45
|
+
"name": "user_counts_by_signup_year"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
EOF
|
|
49
|
+
|
|
50
|
+
TRANSFORM_ID=$(mb transform create --file /tmp/transform.json --profile <name> --json | jq -r '.id')
|
|
51
|
+
mb transform run "$TRANSFORM_ID" --wait --profile <name> --json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Notes:
|
|
55
|
+
|
|
56
|
+
- `<db-id>` comes from `mb database list --profile <name> --json`. Database ids are per-instance — a workspace child re-numbers them independently of the parent.
|
|
57
|
+
- Target `schema` is the **canonical** name (e.g. `public`). In a workspace, the QP rewrites it to the per-workspace isolation schema (`mb__isolation_<hash>_<ws-id>`) at execution time — don't hard-code that prefix.
|
|
58
|
+
- `--wait` on `transform run` polls until status is `succeeded` or `failed`. Without it you only get `{message: "Transform run started", run_id, final: null}` and have to poll yourself.
|
|
59
|
+
- The `--json` envelope is shape-stable: `{message, run_id, final}`. `final` is always present — `null` when `--wait` is omitted or the run never started, otherwise a full `TransformRun` object with `status` and `message`. On a failed run (`final.status` ∈ {`failed`, `timeout`, `canceled`}) the CLI exits 1 and writes a one-line summary `transform run <id> failed` to stderr; the failure detail lives only in `final.message` on stdout, so `jq -r '.final.message'` is where to look.
|
|
60
|
+
- The heredoc with single-quoted `'EOF'` prevents shell from interpolating any `$vars` inside the SQL.
|
|
61
|
+
- `transform create --json` returns the agent-facing compact projection: `{id, name, description, source_type, target: {type, database, schema, name}, target_db_id}`. Read `target.schema`/`target.name` directly off the create output — no follow-up `transform get` needed to verify where the transform will write.
|
|
62
|
+
- If a transform with the same `name` already has a YAML representation on disk under the configured remote-sync repo, `create` mints a `_2` suffix on the exported filename (the new transform gets a fresh `entity_id`; the prior one isn't touched). For "iterate on the same concept" workflows, prefer `transform update <id>` — see "Iterating on a failing transform" below.
|
|
63
|
+
|
|
64
|
+
## Inspect
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
mb transform list --profile <name> --json
|
|
68
|
+
mb transform get <id> --profile <name> --full --json # full transform incl. last run summary
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
After a run, the materialized table is queryable via `mb` (`card create` against it, native query against `<schema>.<name>`, etc.). Columns and types are inferred from the result set; if you change the SELECT shape, drop the table first or the next run will fail on a column-mismatch error.
|
|
72
|
+
|
|
73
|
+
## Inspect runs and cancel an in-flight run
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Recent runs across all transforms (drains all pages by default; cap with --limit):
|
|
77
|
+
mb transform runs --profile <name> --json
|
|
78
|
+
mb transform runs --transform-id <id> --limit 10 --profile <name> --json
|
|
79
|
+
|
|
80
|
+
# Fetch one run by RUN id (NOT transform id — the run id comes from `transform run` or `transform runs`):
|
|
81
|
+
mb transform get-run <run-id> --profile <name> --json
|
|
82
|
+
|
|
83
|
+
# Cancel the currently-running run for a transform:
|
|
84
|
+
mb transform cancel <id> --profile <name> --json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Notes:
|
|
88
|
+
|
|
89
|
+
- `transform runs` and `transform get-run` parse against the same `TransformRun` schema, so `get-run` returns the same per-run shape as one entry of `runs`. The compact projection is `{id, transform_id, status, run_method, start_time, end_time, message}`. Pass `--full` on `get-run` for the hydrated row including `is_active`, `user_id`, `transform_name`, `transform_entity_id`, `checkpoint_*` fields, and a nested `transform: {id, name, …}` block.
|
|
90
|
+
- `transform cancel` takes the **transform** id and 404s with `Endpoint not found — is this a Metabase instance?` if there is no active run. The response shape is `{canceled: true, id: <transform-id>}`.
|
|
91
|
+
- For native-SQL transforms, cancel marks the run as `canceling` but does **not** kill the warehouse query mid-flight — the query runs to completion, then the run lands as `canceled` (or stays `succeeded` if the cancel arrived after the writer committed). For Python transforms the worker is interrupted directly. Don't expect cancel to free warehouse resources instantly on long native queries; expect it to flip state and prevent downstream consumers from treating the result as good.
|
|
92
|
+
- The `--transform-id` filter on `runs` accepts a single integer; the CLI translates to the server's `transform-ids` query vector. To cross-filter multiple transforms, run `transform runs --json` and `jq` post-hoc.
|
|
93
|
+
|
|
94
|
+
## Update body: send only writable keys, never round-trip the GET body
|
|
95
|
+
|
|
96
|
+
`transform update <id>` is **PATCH semantics** — only send the fields you actually want to change. The endpoint accepts exactly these writable keys:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
name, description, source, target, run_trigger,
|
|
100
|
+
tag_ids, collection_id, owner_user_id, owner_email
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Don't paste the output of `transform get` into a `transform update` body.** The GET response carries server-side fields (`id`, `entity_id`, `created_at`, `updated_at`, `creator_id`, `last_run`, `target_db_id`, `target_table_id`, `source_type`, `source_database_id`, `source_readable`, `creator`, `owner`, `table`, …) that the PUT endpoint isn't built to handle. Currently, unknown top-level keys flow into `t2/update!` and produce a leaked H2 SQL error like:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Column "TAGS" not found; SQL statement:
|
|
107
|
+
UPDATE "TRANSFORM" SET "TAGS" = (), "UPDATED_AT" = NOW() WHERE "ID" = ? [42122-214]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Two specific footguns:
|
|
111
|
+
|
|
112
|
+
- **`tags` is not a key on the REST API.** The serdes/YAML representation uses `tags`; the REST contract uses `tag_ids` (an array of integer ids). If you pulled a YAML representation and want to PUT it, translate `tags: [...]` → `tag_ids: [...]` first (or omit it entirely if you're not changing tag membership).
|
|
113
|
+
- **`source_type`, `target_db_id`, `target_table_id`, `entity_id`** are derived/computed by the server. They appear in GET responses for the agent's benefit; the server doesn't accept them on update.
|
|
114
|
+
|
|
115
|
+
Right shape — patch only what changes:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Rename only:
|
|
119
|
+
mb transform update <id> --body '{"name":"renamed"}' --profile <name> --json
|
|
120
|
+
|
|
121
|
+
# Rewrite the SQL only:
|
|
122
|
+
cat > /tmp/patch.json <<'EOF'
|
|
123
|
+
{ "source": { "type": "query", "query": { "type": "native",
|
|
124
|
+
"database": <db-id>,
|
|
125
|
+
"native": { "query": "SELECT … FROM public.orders" } } } }
|
|
126
|
+
EOF
|
|
127
|
+
mb transform update <id> --file /tmp/patch.json --profile <name> --json
|
|
128
|
+
|
|
129
|
+
# Change tag membership (note: tag_ids, not tags):
|
|
130
|
+
mb transform update <id> --body '{"tag_ids":[1,3]}' --profile <name> --json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If you really must round-trip, project to the writable subset:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
mb transform get <id> --full --profile <name> --json \
|
|
137
|
+
| jq '{name, description, source, target, run_trigger, tag_ids, collection_id, owner_user_id, owner_email}
|
|
138
|
+
| with_entries(select(.value != null))' \
|
|
139
|
+
> /tmp/patch.json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Iterating on a failing transform
|
|
143
|
+
|
|
144
|
+
When `transform run` fails and you want to retry with a fixed body, **prefer `transform update <id> --file body.json` over `transform delete <id>` + `transform create`.** Update keeps the same row, the same `entity_id`, the same materialized table, and the same on-disk YAML filename. Concretely this means:
|
|
145
|
+
|
|
146
|
+
- `git-sync export` produces **one** clean commit containing only the fix, instead of "broken transform" + "remove broken transform" landing as two commits in `git log`.
|
|
147
|
+
- You don't have to chase `_2` suffixes minted when two YAMLs share a `name` on disk (see the `transform create` notes above).
|
|
148
|
+
- The materialized output table either updates in place or, if the SELECT shape changed incompatibly, errors loudly on the next run rather than landing in a parallel `..._2` table the agent has to clean up. (`transform delete-table <id>` resets the column shape if you need a clean slate.)
|
|
149
|
+
|
|
150
|
+
Recipe:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# 1. Try once
|
|
154
|
+
ID=$(mb transform create --file /tmp/t.json --profile <n> --json | jq -r '.id')
|
|
155
|
+
mb transform run "$ID" --wait --profile <n> --json # → failed
|
|
156
|
+
|
|
157
|
+
# 2. Fix the body in place; PATCH only what changed.
|
|
158
|
+
# Source-only patch — keeps name, target, tags untouched on the server.
|
|
159
|
+
cat > /tmp/source-patch.json <<'EOF'
|
|
160
|
+
{ "source": { "type": "query", "query": { "type": "native",
|
|
161
|
+
"database": <db-id>,
|
|
162
|
+
"native": { "query": "<fixed SQL here>" } } } }
|
|
163
|
+
EOF
|
|
164
|
+
mb transform update "$ID" --file /tmp/source-patch.json --profile <n> --json
|
|
165
|
+
|
|
166
|
+
# 3. Re-run
|
|
167
|
+
mb transform run "$ID" --wait --profile <n> --json # → succeeded
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
If you really must `create + delete` instead, do the `delete` **before** the first `git-sync export` so the failed entity never lands in git history. Order matters: agents reflex to "export to checkpoint progress," but for transforms an export of a soft-failed state is mostly noise that needs a follow-up cleanup commit. See the `git-sync` skill, "Read state before mutating" for the ordering rule.
|
|
171
|
+
|
|
172
|
+
## Drop the materialized table (keep the transform)
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
mb transform delete-table <id> --yes --profile <name>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Useful when you've changed the SELECT and want a fresh `CREATE TABLE` on the next run. **`--yes` is required** in non-interactive contexts; without it the command exits with `--yes required to delete non-interactively`.
|
|
179
|
+
|
|
180
|
+
## Delete the transform
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
mb transform delete <id> --yes --profile <name>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Removes the definition. Whether the materialized table is dropped depends on the server — check with `mb table list --db-id <db-id> --profile <name> --json` if it matters. Same `--yes` rule as `delete-table`.
|
|
187
|
+
|
|
188
|
+
## Transform jobs (schedules)
|
|
189
|
+
|
|
190
|
+
A schedule lives in a separate resource (`transform-job`) and references one or more transform ids. Create with the same body-input pattern (`--file body.json`); see `mb transform-job --help` for the verb list. Most ad-hoc agent work is one-off `transform run`, not job authoring.
|
|
191
|
+
|
|
192
|
+
## Don't (transform-specific)
|
|
193
|
+
|
|
194
|
+
- Don't put `transform run` calls in tight polling loops — pass `--wait` and let the CLI handle the polling. Manual loops without `--wait` will hammer the server.
|
|
195
|
+
- Don't author MBQL 4 (the legacy nested `{ type: "query", query: {...} }` shape) by hand — pull a sample with `mb transform get <id> --full --json`. MBQL 5 (`lib/type: "mbql/query"`) **is** authorable by hand thanks to the `mb query --print-schema` + `--dry-run` feedback loop; for non-trivial pipelines you may still prefer building in the UI and exporting.
|
|
196
|
+
- Don't write the workspace isolation schema into `target.schema` or SQL. See the `workspace` skill for the canonical-name rule.
|
|
197
|
+
- Don't paste a `transform get` body into `transform update` — the PUT endpoint only accepts writable keys, and unknown keys (notably `tags`, `source_type`, `entity_id`, `created_at`, `last_run`) leak as raw SQL errors. See "Update body: send only writable keys" above. Use `tag_ids` (not `tags`) on the REST contract.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: viz
|
|
3
|
+
description: Author Metabase `visualization_settings` and pick the right `display` for cards via the `mb` CLI. Covers the display → settings-namespace map (graph.*, pie.*, funnel.*, scalar.*, table.*, …), the column-name-vs-numeric-field-id rule, the `column_settings` JSON-string-key footgun, worked API-form examples per chart family, and the pull-from-UI escape hatch for complex charts. Load whenever shaping how a card renders by hand — "create a bar chart", "make this a line chart", "format this column as currency", "set the pie dimension and metric", "the card renders as a table instead of a chart", "add conditional formatting", or any `visualization_settings` work.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Visualization settings
|
|
8
|
+
|
|
9
|
+
A card has two presentation fields alongside its `dataset_query`:
|
|
10
|
+
|
|
11
|
+
- **`display`** — the chart type (`bar`, `line`, `pie`, `scalar`, `table`, …). One closed set of values; pick from the enum below.
|
|
12
|
+
- **`visualization_settings`** — a free-form map whose keys are **namespaced by `display`** (`graph.*` for bar/line, `pie.*` for pie, …). The server stores almost anything and **silently ignores keys that don't apply** to the chosen `display`.
|
|
13
|
+
|
|
14
|
+
The MBQL pre-flight does **not** validate `visualization_settings` — there is no `--skip-validate` to fail past, because nothing checks it. A `display` typo or a misnamed key is accepted by the API and the card just renders as a default table or drops the setting. So **the feedback loop is read-back, not pre-flight**: after `card create`/`update`, confirm with `mb card get <id> --full --json` (or open the card) that it rendered as intended.
|
|
15
|
+
|
|
16
|
+
The general flag conventions and body-input precedence live in the `core` skill (`mb skills get core`); the `dataset_query` body itself is the `mbql` skill's job (`mb skills get mbql`). This skill is only about how the result is displayed.
|
|
17
|
+
|
|
18
|
+
## `display` decides everything
|
|
19
|
+
|
|
20
|
+
`display` selects which setting namespace is read. Pick `display` first, then only the matching namespace's keys matter.
|
|
21
|
+
|
|
22
|
+
| `display` | Settings namespace(s) | Key columns are named by |
|
|
23
|
+
| ------------------------------------------- | --------------------------------------------------------- | ------------------------ |
|
|
24
|
+
| `bar` `line` `area` `combo` `row` `scatter` | `graph.*`, `series_settings`, `column_settings` | output column **name** |
|
|
25
|
+
| `waterfall` | `graph.*` + `waterfall.*` | output column name |
|
|
26
|
+
| `boxplot` | `graph.*` + `boxplot.*` | output column name |
|
|
27
|
+
| `pie` | `pie.*`, `column_settings` | output column name |
|
|
28
|
+
| `scalar` `number` | `scalar.*` | output column name |
|
|
29
|
+
| `smartscalar` | `scalar.comparisons` | output column name |
|
|
30
|
+
| `funnel` | `funnel.*` | output column name |
|
|
31
|
+
| `gauge` | `gauge.*` | — |
|
|
32
|
+
| `map` | `map.*` | output column name |
|
|
33
|
+
| `pivot` | `pivot_table.*`, `table.*` | output column name |
|
|
34
|
+
| `sankey` | `sankey.*` | output column name |
|
|
35
|
+
| `table` | `table.*`, `column_settings` | output column name |
|
|
36
|
+
| `object` `list` | `column_settings` | output column name |
|
|
37
|
+
| `progress` | `progress.goal`, `progress.color` (sparse — pull from UI) | — |
|
|
38
|
+
| `heading` `text` `link` `iframe` `action` | dashcard-only `virtual_card` (see references) | — |
|
|
39
|
+
|
|
40
|
+
Closed `display` enum (card-level): `table`, `bar`, `line`, `area`, `row`, `pie`, `scalar`, `smartscalar`, `number`, `combo`, `pivot`, `funnel`, `map`, `scatter`, `waterfall`, `progress`, `gauge`, `object`, `list`, `sankey`, `boxplot`. (`heading`/`text`/`link`/`iframe`/`action` are dashcard virtuals, not standalone cards.) An unknown value is accepted by the API but renders nothing useful — typos like `bargraph`/`linechart` are the most common cause of a "why is my chart blank" report.
|
|
41
|
+
|
|
42
|
+
## The rule that trips everyone: settings name **output columns**, by name
|
|
43
|
+
|
|
44
|
+
`graph.dimensions`, `graph.metrics`, `pie.dimension`, `pie.metric`, `scalar.field`, `funnel.metric`, `map.latitude_column`, … all take **output column-name strings** — the names the query _produces_, not field ids. A `count` aggregation outputs the column `count`; a breakout on a field outputs that field's name; a named aggregation outputs its `name`. These strings are **identical in the API form and the portable (git-sync) form** — no numeric-vs-name footgun here.
|
|
45
|
+
|
|
46
|
+
So the names you put in `visualization_settings` come from the query's output, not from `mb field`/`mb table`. If you set `name` on an aggregation (see the `mbql` skill, "Naming aggregation output columns"), use that same string here.
|
|
47
|
+
|
|
48
|
+
## Minimum-viable settings per chart family (API form)
|
|
49
|
+
|
|
50
|
+
Each example is the `visualization_settings` block to pair with the given `display` on a `card create`/`update` body. The `dataset_query` is elided — build it per the `mbql` skill. The output columns referenced (`CATEGORY`, `count`) are whatever the query's breakout/aggregation produce.
|
|
51
|
+
|
|
52
|
+
**Bar / line / area** — one dimension on the x-axis, one or more metrics:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
"display": "bar",
|
|
56
|
+
"visualization_settings": {
|
|
57
|
+
"graph.dimensions": ["CATEGORY"],
|
|
58
|
+
"graph.metrics": ["count"]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
(Switch `display` to `line` or `area` with the same `graph.*` keys. Multiple metrics: `"graph.metrics": ["count", "sum"]`. Stacked: add `"stackable.stack_type": "stacked"`.)
|
|
63
|
+
|
|
64
|
+
**Pie** — one dimension, one metric:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
"display": "pie",
|
|
68
|
+
"visualization_settings": { "pie.dimension": "CATEGORY", "pie.metric": "count" }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Scalar** (single big number) — the field to surface:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
"display": "scalar",
|
|
75
|
+
"visualization_settings": { "scalar.field": "count" }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Table** — column order/visibility plus per-column formatting:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
"display": "table",
|
|
82
|
+
"visualization_settings": {
|
|
83
|
+
"table.columns": [
|
|
84
|
+
{ "name": "CATEGORY", "enabled": true },
|
|
85
|
+
{ "name": "count", "enabled": true }
|
|
86
|
+
],
|
|
87
|
+
"column_settings": {
|
|
88
|
+
"[\"name\",\"count\"]": { "column_title": "Orders" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
An **empty** `"visualization_settings": {}` is valid for any `display` — Metabase falls back to sensible defaults (it auto-picks dimensions/metrics for a simple aggregate). Set keys only to override the defaults.
|
|
94
|
+
|
|
95
|
+
## `column_settings`: the JSON-string-key footgun
|
|
96
|
+
|
|
97
|
+
`column_settings` is a map **whose keys are themselves JSON-encoded arrays** — so inside a JSON body the inner quotes must be escaped. The key is a _string_, never an object.
|
|
98
|
+
|
|
99
|
+
- **Prefer the name form:** `["name", "<output column name>"]` → in a JSON body, `"[\"name\",\"count\"]"`. This is the canonical key Metabase writes (`getColumnKey`), and it's **identical in API and portable form**. Use it unless you have a reason not to.
|
|
100
|
+
- **Ref form (legacy order!):** `["ref", ["field", <id>, <opts>]]`. The inner field ref here uses the **legacy MBQL-4 order** `["field", id, options]` (id **second**) — _not_ the MBQL-5 order `["field", {options}, id]` you use in `dataset_query`. In the API form `<id>` is the **numeric** field id; the portable form uses a name path. Because the order differs from MBQL 5, this form is easy to get wrong — reach for the name form instead.
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
"column_settings": {
|
|
104
|
+
"[\"name\",\"TOTAL\"]": { "number_style": "currency", "currency": "USD", "decimals": 2 },
|
|
105
|
+
"[\"name\",\"CREATED_AT\"]": { "date_style": "MMMM D, YYYY" }
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The exhaustive per-column key list (number/date/link formatting, `view_as`, click behavior) is in the references file.
|
|
110
|
+
|
|
111
|
+
## Escape hatch: pull a real card instead of authoring from scratch
|
|
112
|
+
|
|
113
|
+
For anything beyond a single dimension + metric — combo charts, conditional formatting, pivot splits, click behavior, series colors — the cheapest **correct** path is to build it once in the Metabase UI and copy the result:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
mb card get <id> --full --json | jq '.visualization_settings'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Paste that block into your `card create`/`update` body. The server produced it, so it's guaranteed valid for that `display`. This beats guessing keys from memory, and it's token-cheap (no schema dump).
|
|
120
|
+
|
|
121
|
+
## Full key catalog
|
|
122
|
+
|
|
123
|
+
The body above covers the high-frequency 90%. The complete per-display key tables — every `graph.*`/`pie.*`/`table.*`/… key, series settings, conditional formatting rules, pivot splits, the full `column_settings` formatting vocabulary, virtual-card (heading/text/link/iframe) settings, and click behavior — live in this skill's references file. Load it on demand, not by default:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
mb skills get viz --full # appends references/settings.md to this body
|
|
127
|
+
mb skills path viz # → the skill dir; then Read references/settings.md
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Don't
|
|
131
|
+
|
|
132
|
+
- Don't invent `display` values (`bargraph`, `linechart`, `histogram`) — use the closed enum; the API accepts the typo and renders nothing.
|
|
133
|
+
- Don't put numeric field ids in `graph.dimensions`/`pie.metric`/`scalar.field` etc. — they take **output column-name strings**.
|
|
134
|
+
- Don't write a `column_settings` key as an object — it's a JSON **string** (`"[\"name\",\"COL\"]"`), inner quotes escaped.
|
|
135
|
+
- Don't use the MBQL-5 field-ref order inside a `column_settings` `["ref", …]` key — that key uses the **legacy** `["field", id, opts]` order. Prefer the `["name", …]` form and sidestep it.
|
|
136
|
+
- Don't expect a pre-flight to catch viz mistakes — there is none. Verify by reading the card back.
|
|
137
|
+
- Don't hand-author complex charts when you can pull a working `visualization_settings` from a UI-built card.
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# visualization_settings — full key catalog
|
|
2
|
+
|
|
3
|
+
Exhaustive reference for `visualization_settings`. The SKILL.md body covers the
|
|
4
|
+
common cases; this file is the long tail. Keys are grouped by the `display` they
|
|
5
|
+
apply to (see the display → namespace map in the body).
|
|
6
|
+
|
|
7
|
+
**Form note.** Every key name and value enum below is identical in the API/numeric
|
|
8
|
+
form (`mb card create`) and the portable git-sync YAML form. The only form-specific
|
|
9
|
+
constructs are (1) `column_settings` `["ref", …]` keys, where the inner field id is
|
|
10
|
+
numeric in the API form and a name-path in the portable form, and (2) click-behavior
|
|
11
|
+
dimension targets, same rule. All the column-naming keys (`graph.dimensions`,
|
|
12
|
+
`pie.dimension`, `table.columns[].name`, …) are output column-name strings in both.
|
|
13
|
+
|
|
14
|
+
JSON-body reminder: examples here are shown as YAML/JSON fragments. In a real
|
|
15
|
+
`--body`/`--file` JSON payload, `column_settings` keys are JSON **strings** with
|
|
16
|
+
escaped inner quotes: `"[\"name\",\"TOTAL\"]"`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Common
|
|
21
|
+
|
|
22
|
+
| Setting | Type | Description |
|
|
23
|
+
| --------------------- | ------- | ------------------------------------------------------------------- |
|
|
24
|
+
| `column_settings` | map | Per-column formatting, keyed by column-reference string (see below) |
|
|
25
|
+
| `card.title` | string | Override the card's display title on a dashboard |
|
|
26
|
+
| `card.description` | string | Override the description |
|
|
27
|
+
| `dashcard.background` | boolean | Show/hide the dashcard background (dashcards only) |
|
|
28
|
+
|
|
29
|
+
## Graph (`bar`, `line`, `area`, `combo`, `scatter`, `waterfall`, `row`, `boxplot`)
|
|
30
|
+
|
|
31
|
+
| Setting | Type | Values / notes |
|
|
32
|
+
| --------------------------------------- | -------------- | ------------------------------------------------------------------------ |
|
|
33
|
+
| `graph.dimensions` | array | Dimension (x-axis) output column names |
|
|
34
|
+
| `graph.metrics` | array | Metric (y-axis) output column names |
|
|
35
|
+
| `graph.series_order` | array | Explicit series display order |
|
|
36
|
+
| `graph.show_values` | boolean | Show value labels on data points |
|
|
37
|
+
| `graph.label_values_frequency` | string | `"fit"`, `"all"` |
|
|
38
|
+
| `graph.show_stack_values` | string | `"total"`, `"individual"`, `"all"` |
|
|
39
|
+
| `graph.x_axis.title_text` | string | X-axis title |
|
|
40
|
+
| `graph.x_axis.scale` | string | `"ordinal"`, `"histogram"`, `"timeseries"`, `"linear"`, `"pow"`, `"log"` |
|
|
41
|
+
| `graph.x_axis.axis_enabled` | boolean/string | `true`, `false`, `"compact"`, `"rotate-45"`, `"rotate-90"` |
|
|
42
|
+
| `graph.y_axis.title_text` | string | Y-axis title |
|
|
43
|
+
| `graph.y_axis.scale` | string | `"linear"`, `"pow"`, `"log"` |
|
|
44
|
+
| `graph.y_axis.auto_range` | boolean | Auto-scale Y axis |
|
|
45
|
+
| `graph.y_axis.min` / `graph.y_axis.max` | number | Y bounds when `auto_range` is false |
|
|
46
|
+
| `graph.show_goal` | boolean | Show goal line |
|
|
47
|
+
| `graph.goal_value` | number | Goal line value |
|
|
48
|
+
| `graph.goal_label` | string | Goal line label |
|
|
49
|
+
| `graph.show_trendline` | boolean | Show trend line |
|
|
50
|
+
| `graph.max_categories_enabled` | boolean | Limit number of categories |
|
|
51
|
+
| `graph.max_categories` | number | Maximum categories shown |
|
|
52
|
+
| `graph.other_category_aggregation_fn` | string | `"sum"`, `"avg"`, `"min"`, `"max"` |
|
|
53
|
+
| `stackable.stack_type` | string | `null`, `"stacked"`, `"normalized"` |
|
|
54
|
+
|
|
55
|
+
### Series settings
|
|
56
|
+
|
|
57
|
+
Per-series overrides keyed by series name (the metric column name, or the breakout
|
|
58
|
+
value for a split series):
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
series_settings:
|
|
62
|
+
Revenue:
|
|
63
|
+
display: line # override this series' type in a combo chart
|
|
64
|
+
color: "#509EE3"
|
|
65
|
+
"line.style": solid # "solid", "dashed", "dotted"
|
|
66
|
+
"line.size": normal # "S", "M", "L"
|
|
67
|
+
"line.interpolate": linear # "linear", "cardinal", "step-before", "step-after"
|
|
68
|
+
"line.missing": interpolate # "interpolate", "zero", "none"
|
|
69
|
+
"line.marker_enabled": true
|
|
70
|
+
axis: left # "left", "right"
|
|
71
|
+
show_series_values: true
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Waterfall extras (`display: waterfall`)
|
|
75
|
+
|
|
76
|
+
| Setting | Type | Description |
|
|
77
|
+
| -------------------------- | ------- | ----------------------- |
|
|
78
|
+
| `waterfall.increase_color` | string | Color for increases |
|
|
79
|
+
| `waterfall.decrease_color` | string | Color for decreases |
|
|
80
|
+
| `waterfall.total_color` | string | Color for the total bar |
|
|
81
|
+
| `waterfall.show_total` | boolean | Show the total bar |
|
|
82
|
+
|
|
83
|
+
### BoxPlot extras (`display: boxplot`)
|
|
84
|
+
|
|
85
|
+
| Setting | Type | Values |
|
|
86
|
+
| ---------------------- | ------- | -------------------------------------- |
|
|
87
|
+
| `boxplot.whisker_type` | string | `"min-max"`, `"tukey"`, `"percentile"` |
|
|
88
|
+
| `boxplot.points_mode` | string | `"none"`, `"outliers"`, `"all"` |
|
|
89
|
+
| `boxplot.show_mean` | boolean | Show mean marker |
|
|
90
|
+
|
|
91
|
+
## Pie (`display: pie`)
|
|
92
|
+
|
|
93
|
+
| Setting | Type | Values |
|
|
94
|
+
| ------------------------ | ------- | ----------------------------------------- |
|
|
95
|
+
| `pie.dimension` | string | Dimension column name |
|
|
96
|
+
| `pie.metric` | string | Metric column name |
|
|
97
|
+
| `pie.show_legend` | boolean | |
|
|
98
|
+
| `pie.show_total` | boolean | Show total in center |
|
|
99
|
+
| `pie.percent_visibility` | string | `"off"`, `"legend"`, `"inside"`, `"both"` |
|
|
100
|
+
| `pie.slice_threshold` | number | Min percentage to show as its own slice |
|
|
101
|
+
| `pie.colors` | object | Color map keyed by dimension value |
|
|
102
|
+
|
|
103
|
+
## Scalar / Number (`display: scalar`, `number`)
|
|
104
|
+
|
|
105
|
+
| Setting | Type | Values |
|
|
106
|
+
| --------------------------------- | ------- | ------------------------------- |
|
|
107
|
+
| `scalar.field` | string | Output column to display |
|
|
108
|
+
| `scalar.switch_positive_negative` | boolean | Invert positive/negative colors |
|
|
109
|
+
| `scalar.compact_primary_number` | string | `"auto"`, `"yes"`, `"no"` |
|
|
110
|
+
|
|
111
|
+
### Smart scalar (`display: smartscalar`)
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
scalar.comparisons:
|
|
115
|
+
- id: comp1
|
|
116
|
+
type: previousPeriod # vs. previous time period
|
|
117
|
+
- id: comp2
|
|
118
|
+
type: previousValue # vs. previous value
|
|
119
|
+
- id: comp3
|
|
120
|
+
type: periodsAgo # vs. N periods ago
|
|
121
|
+
value: 12
|
|
122
|
+
- id: comp4
|
|
123
|
+
type: staticNumber # vs. fixed number
|
|
124
|
+
value: 1000
|
|
125
|
+
label: Target
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Funnel (`display: funnel`)
|
|
129
|
+
|
|
130
|
+
| Setting | Type | Values |
|
|
131
|
+
| ------------------ | ------ | --------------------- |
|
|
132
|
+
| `funnel.dimension` | string | Dimension column |
|
|
133
|
+
| `funnel.metric` | string | Metric column |
|
|
134
|
+
| `funnel.type` | string | `"funnel"` or `"bar"` |
|
|
135
|
+
| `funnel.rows` | array | Row order definitions |
|
|
136
|
+
|
|
137
|
+
## Gauge (`display: gauge`)
|
|
138
|
+
|
|
139
|
+
| Setting | Type | Description |
|
|
140
|
+
| ---------------------- | ----- | ------------------------------------------- |
|
|
141
|
+
| `gauge.segments` | array | Segments, each `{ min, max, color, label }` |
|
|
142
|
+
| `gauge.segment_colors` | array | Segment colors |
|
|
143
|
+
|
|
144
|
+
## Map (`display: map`)
|
|
145
|
+
|
|
146
|
+
| Setting | Type | Values |
|
|
147
|
+
| ---------------------------------------------- | ------ | -------------------------------- |
|
|
148
|
+
| `map.type` | string | `"region"`, `"pin"`, `"grid"` |
|
|
149
|
+
| `map.latitude_column` | string | Latitude column name |
|
|
150
|
+
| `map.longitude_column` | string | Longitude column name |
|
|
151
|
+
| `map.metric_column` | string | Metric column for coloring |
|
|
152
|
+
| `map.region` | string | Region map identifier |
|
|
153
|
+
| `map.pin_type` | string | `"tiles"`, `"markers"`, `"heat"` |
|
|
154
|
+
| `map.colors` | array | Color scale |
|
|
155
|
+
| `map.zoom` | number | Initial zoom level |
|
|
156
|
+
| `map.center_latitude` / `map.center_longitude` | number | Map center |
|
|
157
|
+
|
|
158
|
+
## Table (`display: table`)
|
|
159
|
+
|
|
160
|
+
| Setting | Type | Description |
|
|
161
|
+
| ------------------------- | ------- | --------------------------------------------------------- |
|
|
162
|
+
| `table.columns` | array | Column order + visibility; each entry `{ name, enabled }` |
|
|
163
|
+
| `table.column_formatting` | array | Conditional formatting rules (see below) |
|
|
164
|
+
| `table.pivot` | boolean | Enable in-table pivot mode |
|
|
165
|
+
| `table.pivot_column` | string | Column to pivot on |
|
|
166
|
+
| `table.cell_column` | string | Column used for cell values (pivot mode) |
|
|
167
|
+
|
|
168
|
+
### Conditional formatting (`table.column_formatting`)
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
table.column_formatting:
|
|
172
|
+
- columns: [Total]
|
|
173
|
+
type: single # "single" or "range"
|
|
174
|
+
operator: ">" # "=", "!=", "<", ">", "<=", ">=", "is-null", "not-null"
|
|
175
|
+
value: 100
|
|
176
|
+
color: "#84BB4C"
|
|
177
|
+
highlight_row: false
|
|
178
|
+
- columns: [Rating]
|
|
179
|
+
type: range
|
|
180
|
+
colors: ["#ED6E6E", "#F9CF48", "#84BB4C"]
|
|
181
|
+
min_type: custom # "min", "max", "custom"
|
|
182
|
+
min_value: 1
|
|
183
|
+
max_type: custom
|
|
184
|
+
max_value: 5
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Pivot table (`display: pivot`)
|
|
188
|
+
|
|
189
|
+
| Setting | Type | Description |
|
|
190
|
+
| -------------------------------- | ------- | --------------------------------------------------------------- |
|
|
191
|
+
| `pivot_table.column_split` | object | `{ rows: [...names], columns: [...names], values: [...names] }` |
|
|
192
|
+
| `pivot_table.collapsed_rows` | object | `{ rows: [...collapsed_keys], value: [] }` |
|
|
193
|
+
| `pivot_table.show_row_totals` | boolean | |
|
|
194
|
+
| `pivot_table.show_column_totals` | boolean | |
|
|
195
|
+
|
|
196
|
+
## Sankey (`display: sankey`)
|
|
197
|
+
|
|
198
|
+
| Setting | Type | Values |
|
|
199
|
+
| ------------------------- | ------- | -------------------------------------------- |
|
|
200
|
+
| `sankey.source` | string | Source column |
|
|
201
|
+
| `sankey.target` | string | Target column |
|
|
202
|
+
| `sankey.value` | string | Value column |
|
|
203
|
+
| `sankey.node_align` | string | `"left"`, `"right"`, `"center"`, `"justify"` |
|
|
204
|
+
| `sankey.show_edge_labels` | boolean | |
|
|
205
|
+
|
|
206
|
+
## Column settings
|
|
207
|
+
|
|
208
|
+
`column_settings` is keyed by a JSON-encoded column reference. Prefer the
|
|
209
|
+
`["name", "<output column name>"]` form — it is the canonical key Metabase writes and
|
|
210
|
+
is identical across forms. The `["ref", ["field", <id>, <opts>]]` form uses the
|
|
211
|
+
**legacy** field-ref order (id second); in the API form `<id>` is the numeric field id.
|
|
212
|
+
|
|
213
|
+
Per-column keys (apply under any column key):
|
|
214
|
+
|
|
215
|
+
| Key | Type | Values |
|
|
216
|
+
| ------------------- | ----------- | ------------------------------------------------------ |
|
|
217
|
+
| `column_title` | string | Override the column header |
|
|
218
|
+
| `number_style` | string | `"decimal"`, `"currency"`, `"percent"`, `"scientific"` |
|
|
219
|
+
| `currency` | string | ISO code, e.g. `"USD"` |
|
|
220
|
+
| `currency_style` | string | `"symbol"`, `"code"`, `"name"` |
|
|
221
|
+
| `number_separators` | string | e.g. `".,"` (decimal + thousands) |
|
|
222
|
+
| `decimals` | number | Fixed decimal places |
|
|
223
|
+
| `scale` | number | Multiply values by this factor |
|
|
224
|
+
| `prefix` / `suffix` | string | Affixes around the value |
|
|
225
|
+
| `date_style` | string | moment.js format, e.g. `"MMMM D, YYYY"` |
|
|
226
|
+
| `date_separator` | string | e.g. `"/"` |
|
|
227
|
+
| `date_abbreviate` | boolean | |
|
|
228
|
+
| `time_enabled` | string/null | `null`, `"minutes"`, `"seconds"`, `"milliseconds"` |
|
|
229
|
+
| `time_style` | string | e.g. `"HH:mm"`, `"h:mm A"` |
|
|
230
|
+
| `view_as` | string | `"link"`, `"image"`, `"email"`, `"auto"` |
|
|
231
|
+
| `link_text` | string | Display text when `view_as: link` |
|
|
232
|
+
| `link_url` | string | URL template; `{{value}}` interpolates the cell value |
|
|
233
|
+
| `click_behavior` | object | Per-column click behavior (see below) |
|
|
234
|
+
|
|
235
|
+
```yaml
|
|
236
|
+
column_settings:
|
|
237
|
+
'["name","TOTAL"]':
|
|
238
|
+
number_style: currency
|
|
239
|
+
currency: USD
|
|
240
|
+
currency_style: symbol
|
|
241
|
+
decimals: 2
|
|
242
|
+
column_title: "Total Revenue"
|
|
243
|
+
'["name","CREATED_AT"]':
|
|
244
|
+
date_style: "MMMM D, YYYY"
|
|
245
|
+
time_enabled: null
|
|
246
|
+
'["name","EMAIL"]':
|
|
247
|
+
view_as: link
|
|
248
|
+
link_text: "Send email"
|
|
249
|
+
link_url: "mailto:{{value}}"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Virtual card settings (dashcards only, `card_id: null`)
|
|
253
|
+
|
|
254
|
+
| `virtual_card.display` | Extra keys |
|
|
255
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
256
|
+
| `heading` | `text` (the heading string) |
|
|
257
|
+
| `text` | `text` (markdown; `{{param}}` placeholders wired via dashcard `parameter_mappings`, target `[text-tag, name]`) |
|
|
258
|
+
| `link` | `link.url`, or `link.entity` `{ id, model }` where model ∈ `question`/`dashboard`/`collection`/`database`/`table` |
|
|
259
|
+
| `iframe` | `iframe` (the `<iframe …>` HTML string) |
|
|
260
|
+
| `placeholder` | — |
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
visualization_settings:
|
|
264
|
+
virtual_card:
|
|
265
|
+
display: text
|
|
266
|
+
text: "**Bold** and _italic_ markdown content"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Click behavior
|
|
270
|
+
|
|
271
|
+
Stored at `visualization_settings.click_behavior` (whole dashcard) or per column at
|
|
272
|
+
`column_settings[<key>].click_behavior`.
|
|
273
|
+
|
|
274
|
+
| `type` | Description |
|
|
275
|
+
| ------------- | -------------------------------------------- |
|
|
276
|
+
| `actionMenu` | Default drill-through menu (no config) |
|
|
277
|
+
| `crossfilter` | Filter the dashboard using the clicked value |
|
|
278
|
+
| `link` | Navigate to a URL, question, or dashboard |
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
# Link to URL — {{column}} = clicked row value, {{filter:param}} = dashboard parameter
|
|
282
|
+
click_behavior:
|
|
283
|
+
type: link
|
|
284
|
+
linkType: url
|
|
285
|
+
linkTemplate: "https://example.com/orders/{{ORDER_ID}}?status={{filter:status}}"
|
|
286
|
+
linkTextTemplate: "View Order {{ORDER_ID}}"
|
|
287
|
+
|
|
288
|
+
# Link to another dashboard/question — targetId is the entity_id of the target
|
|
289
|
+
click_behavior:
|
|
290
|
+
type: link
|
|
291
|
+
linkType: dashboard # or "question"
|
|
292
|
+
targetId: Q_jD-f-9clKLFZ2TfUG2h
|
|
293
|
+
parameterMapping:
|
|
294
|
+
target-param-uuid:
|
|
295
|
+
id: target-param-uuid
|
|
296
|
+
source: { id: USER_ID, name: User ID, type: column }
|
|
297
|
+
target: { id: target-param-uuid, type: parameter }
|
|
298
|
+
|
|
299
|
+
# Crossfilter — map a clicked column to dashboard parameters
|
|
300
|
+
click_behavior:
|
|
301
|
+
type: crossfilter
|
|
302
|
+
parameterMapping:
|
|
303
|
+
param-uuid:
|
|
304
|
+
id: param-uuid
|
|
305
|
+
source: { id: CATEGORY, name: Category, type: column }
|
|
306
|
+
target: { id: param-uuid, type: parameter }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`parameterMapping` entries: `source` is where the value comes from
|
|
310
|
+
(`{ id, name, type }`, type `"column"`/`"parameter"`); `target` is where it goes
|
|
311
|
+
(`{ id, type }`, type `"parameter"`/`"dimension"`/`"variable"`). A `dimension` target
|
|
312
|
+
carries a `dimension` array — the same target shape as a dashboard parameter mapping.
|