@metabase/cli 0.1.5 → 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.
Files changed (217) hide show
  1. package/README.md +115 -102
  2. package/dist/{add-collection-C_iovi9i.mjs → add-collection-BU8r3r2M.mjs} +9 -4
  3. package/dist/add-collection-C0w6ACQF.mjs +11 -0
  4. package/dist/{archive-Dvzrmdbk.mjs → archive-BNinrUak.mjs} +9 -8
  5. package/dist/{archive-WaEW85NB.mjs → archive-C1enZgKV.mjs} +8 -7
  6. package/dist/archive-CDA0KxL8.mjs +40 -0
  7. package/dist/{archive-BKPO8lEO.mjs → archive-CRhiBpPJ.mjs} +9 -8
  8. package/dist/{archive-DdaP94H3.mjs → archive-DMPS8Kih.mjs} +9 -8
  9. package/dist/archive-lWgqiFAt.mjs +40 -0
  10. package/dist/auth-CzXb_zB2.mjs +19 -0
  11. package/dist/{body-XtR7-uCO.mjs → body-DjdFxjpg.mjs} +4 -4
  12. package/dist/{branches-XUY4JY-X.mjs → branches-B1WRfG7-.mjs} +11 -7
  13. package/dist/{cancel-BrUVO_ax.mjs → cancel-Dl_Ho056.mjs} +7 -6
  14. package/dist/{cancel-task-oXheTOB6.mjs → cancel-task-CdigdCaO.mjs} +11 -7
  15. package/dist/capabilities-7e9MgquN.mjs +29 -0
  16. package/dist/card-DP4rfoOi.mjs +21 -0
  17. package/dist/{card-CQxvHeyP.mjs → card-DlCAaAPq.mjs} +1 -1
  18. package/dist/{cards-CONTTAG9.mjs → cards-BGiJS675.mjs} +8 -7
  19. package/dist/cli.mjs +264 -44
  20. package/dist/collection-tY18ezvn.mjs +21 -0
  21. package/dist/{predicates-CGO17Q15.mjs → command-augment-BH9qgQ5u.mjs} +66 -14
  22. package/dist/create-BNiva__H.mjs +52 -0
  23. package/dist/{create-Ca9lIDwP.mjs → create-BTcpaop_.mjs} +9 -8
  24. package/dist/{create-V-q2rU0T.mjs → create-BYlIju0b.mjs} +14 -12
  25. package/dist/{create-DZxUeqdf.mjs → create-Be_0Vier.mjs} +10 -9
  26. package/dist/{create-kYpjobrq.mjs → create-CHF313Qg.mjs} +13 -9
  27. package/dist/{create-swbIXdo5.mjs → create-CwGtmwqm.mjs} +14 -12
  28. package/dist/{create-Dq25vsMu.mjs → create-CzzrbL0u.mjs} +10 -9
  29. package/dist/{create-Le3Bqn7b.mjs → create-DGth_uOp.mjs} +14 -12
  30. package/dist/{create-branch-D5u14AxL.mjs → create-branch-DKZkoQ64.mjs} +11 -7
  31. package/dist/{create-Cs2xntFG.mjs → create-dhxPxfF3.mjs} +16 -14
  32. package/dist/{credentials-BIQ1cEzM.mjs → credentials-dzeq7ckm.mjs} +12 -11
  33. package/dist/{current-task-DCq7rk9V.mjs → current-task-CCRzm0_7.mjs} +11 -7
  34. package/dist/dashboard-ChM_Tu0l.mjs +22 -0
  35. package/dist/{dashboard-CnMD04PQ.mjs → dashboard-FY5UzJ_Z.mjs} +2 -1
  36. package/dist/{database-BSvzYlRe.mjs → database-CIXwHKjK.mjs} +3 -3
  37. package/dist/{database-vvig8k4x.mjs → database-lH-B3G1I.mjs} +1 -1
  38. package/dist/db-DrQn_i3W.mjs +22 -0
  39. package/dist/{remove-C6bS0Z6w.mjs → delete-CM3jnAeQ.mjs} +21 -20
  40. package/dist/{delete-CUx6RT9e.mjs → delete-Dimc-2y8.mjs} +9 -8
  41. package/dist/{delete-VTAS9EUt.mjs → delete-ZjnV35OJ.mjs} +9 -8
  42. package/dist/{delete-runtime-DfFMWJJ6.mjs → delete-runtime-B6RQo_pw.mjs} +5 -3
  43. package/dist/{delete-table-DzUneMKe.mjs → delete-table-agZJpivt.mjs} +9 -8
  44. package/dist/{deprovision-CpJfGgCt.mjs → deprovision-CwxcIT3k.mjs} +16 -12
  45. package/dist/{dirty-nkAOXxgC.mjs → dirty-D4d0yHqj.mjs} +11 -7
  46. package/dist/{docker-D5FTIoD0.mjs → docker-Oq80q3tu.mjs} +4 -4
  47. package/dist/{translate-Cqsd0Px5.mjs → eid-BXzaQh0o.mjs} +37 -22
  48. package/dist/error-C9S6PN3-.mjs +190 -0
  49. package/dist/{export-BWvY7X_G.mjs → export-DTygoXBP.mjs} +17 -16
  50. package/dist/field-Z6Pcxf4n.mjs +19 -0
  51. package/dist/{fields-dH16G5UV.mjs → fields-CoQi99gv.mjs} +9 -8
  52. package/dist/{get-BnBRKHr7.mjs → get-Bzys7vgp.mjs} +8 -7
  53. package/dist/{get-B7i_nYJB.mjs → get-C2p383Qc.mjs} +8 -7
  54. package/dist/{get-D96QEU49.mjs → get-C3HdQ91a.mjs} +8 -7
  55. package/dist/{get-DNN1X2gN.mjs → get-CP3Z3NiH.mjs} +9 -8
  56. package/dist/{get-CACaBFLt.mjs → get-C_w1kvN3.mjs} +9 -8
  57. package/dist/{get-D8e_RzZ0.mjs → get-CzuzeKSe.mjs} +10 -9
  58. package/dist/{get-C6SR3A9t.mjs → get-D3SbEQSE.mjs} +10 -9
  59. package/dist/{get-7macOPAI.mjs → get-DFxZXaKz.mjs} +7 -7
  60. package/dist/{get-DAWofnzK.mjs → get-DQTZG_NP.mjs} +8 -7
  61. package/dist/{get-BcqxMVC1.mjs → get-DSWFjy7O.mjs} +8 -7
  62. package/dist/{get-R7OaVL_t.mjs → get-Ddr0XLh7.mjs} +8 -7
  63. package/dist/{get-B08K82JV.mjs → get-Hc93A0Yz.mjs} +8 -7
  64. package/dist/{get-CKxlhMy1.mjs → get-lb7q3JYs.mjs} +7 -6
  65. package/dist/get-run-B7sKdaDU.mjs +38 -0
  66. package/dist/git-sync-CiGAad76.mjs +28 -0
  67. package/dist/{has-remote-changes-BAnIXQXU.mjs → has-remote-changes-BY10-nnE.mjs} +11 -7
  68. package/dist/{import-CfdPEMng.mjs → import-CiMz4Wz-.mjs} +17 -16
  69. package/dist/{input-BQ-BZA8h.mjs → input-cMSEqISy.mjs} +7 -4
  70. package/dist/{is-dirty-CZWcG0vj.mjs → is-dirty-BZOaryxT.mjs} +9 -4
  71. package/dist/is-dirty-Ume4oV0j.mjs +10 -0
  72. package/dist/{items-DqwahOKf.mjs → items-BWfvkY-J.mjs} +9 -8
  73. package/dist/key-C2XG394c.mjs +17 -0
  74. package/dist/license-Dxarh-gG.mjs +17 -0
  75. package/dist/{list-vF4EneaE.mjs → list--OYdUTtu.mjs} +7 -6
  76. package/dist/{list-yxVAE1S7.mjs → list-2j7GsXsl.mjs} +7 -6
  77. package/dist/{list-D41gfkKb.mjs → list-BI4zr8LW.mjs} +10 -8
  78. package/dist/{list-BpNU1neq.mjs → list-Brgh-Z2v.mjs} +8 -6
  79. package/dist/{list-ViT2KWhv.mjs → list-C3hfovHv.mjs} +7 -6
  80. package/dist/{list-CQkDqphl.mjs → list-CL7eCOQE.mjs} +7 -6
  81. package/dist/{list-L63TpX1t.mjs → list-Clz5igWg.mjs} +7 -7
  82. package/dist/list-D4sFiqX8.mjs +173 -0
  83. package/dist/{list-oftHLFbE.mjs → list-DXH7TlkU.mjs} +9 -7
  84. package/dist/{list-BqNMpIXy.mjs → list-DZ8fNUoQ.mjs} +9 -8
  85. package/dist/{list-Bkd7Nbds.mjs → list-SOG0whQ-.mjs} +7 -6
  86. package/dist/{list-J277Qtki.mjs → list-d58BprgJ.mjs} +7 -6
  87. package/dist/{list-DJcGwJ4W.mjs → list-sD5N3fGk.mjs} +9 -8
  88. package/dist/{list-DBOYoJtA.mjs → list-zSO0DMw-.mjs} +10 -6
  89. package/dist/{login-D1nZwgKv.mjs → login-Bm2AnCez.mjs} +65 -80
  90. package/dist/{logout-DD4q5whi.mjs → logout-BlyRJODO.mjs} +8 -7
  91. package/dist/{logs-Ci3mJE2z.mjs → logs-CywPikkL.mjs} +9 -8
  92. package/dist/{manifest-CGM7XNLC.mjs → manifest-BBR46KFM.mjs} +15 -15
  93. package/dist/measure-C44EK_xt.mjs +20 -0
  94. package/dist/{measure-BEQfnLdN.mjs → measure-ClESGxIb.mjs} +2 -2
  95. package/dist/{metadata-BDat-jN9.mjs → metadata-B8ZSF9LA.mjs} +10 -9
  96. package/dist/{metadata-29_qlqbz.mjs → metadata-DqiI2q9q.mjs} +9 -8
  97. package/dist/parse-enum-CrEWOhuY.mjs +11 -0
  98. package/dist/{parse-id-CysSaCbf.mjs → parse-id-lk_K-CEF.mjs} +1 -1
  99. package/dist/{parse-ref-D1yeDOn8.mjs → parse-ref-BiETXmvm.mjs} +1 -1
  100. package/dist/{parse-schemas-B10n01ez.mjs → parse-schemas-BqUdWUwq.mjs} +2 -2
  101. package/dist/{path-DLByFMMA.mjs → path-AEtZ3mBq.mjs} +7 -7
  102. package/dist/{poll-p9Y7-JEQ.mjs → poll-DHKDpCiq.mjs} +2 -2
  103. package/dist/{poll-task-BQe0NvJZ.mjs → poll-task-Cooi0lQV.mjs} +3 -20
  104. package/dist/{preflight-CvFu0Cct.mjs → preflight-aXV5LyDs.mjs} +4 -4
  105. package/dist/{process-zJeVJZTM.mjs → process-C7V8LJ-j.mjs} +1 -1
  106. package/dist/{prompt-DgDNy_Pc.mjs → prompt-CFKoys7k.mjs} +3 -1
  107. package/dist/{provision-BP-b4Are.mjs → provision-UWcNDoDe.mjs} +29 -24
  108. package/dist/{ps-BxQdpkr5.mjs → ps-CJU0EbrC.mjs} +5 -3
  109. package/dist/ps-DEroLgbI.mjs +11 -0
  110. package/dist/{query-CFH4nBlK.mjs → query-AaKzYnTY.mjs} +9 -8
  111. package/dist/{query-C7zTlFJA.mjs → query-BlsVNZpD.mjs} +15 -13
  112. package/dist/{remove-BuWxx3hY.mjs → remove-BFWun0e8.mjs} +9 -8
  113. package/dist/{remove-collection-Bc4roCq0.mjs → remove-collection-CoCmrrQs.mjs} +13 -9
  114. package/dist/{render-DuoDUTVL.mjs → render-CfznwleY.mjs} +15 -17
  115. package/dist/render-OQn3iRsI.mjs +32 -0
  116. package/dist/{rescan-values-DabyRYQ_.mjs → rescan-values-C0FDsjT7.mjs} +10 -9
  117. package/dist/{run-Cl-9RtC4.mjs → run-B4Wn43zm.mjs} +10 -9
  118. package/dist/{runs-BH6s1Zao.mjs → runs-Bbaszr18.mjs} +9 -8
  119. package/dist/{runtime-CDu6fykq.mjs → runtime-Dmv5VtUK.mjs} +657 -428
  120. package/dist/{schema-tables-i58wp_p3.mjs → schema-tables-CaWinbuK.mjs} +9 -8
  121. package/dist/{schemas-_m8RYRl9.mjs → schemas-DUgGpAyB.mjs} +7 -6
  122. package/dist/{search-DObOsjbP.mjs → search-BLrBXLUk.mjs} +12 -16
  123. package/dist/segment-B3Uwwcsm.mjs +20 -0
  124. package/dist/{set-CJA9dpK6.mjs → set-B8cUbRLD.mjs} +13 -12
  125. package/dist/{set-CwVWeAsi.mjs → set-DfGsta5O.mjs} +11 -10
  126. package/dist/{setting-Czy4ws6h.mjs → setting-D2p2MA7f.mjs} +3 -3
  127. package/dist/{setup-DqBOe3HZ.mjs → setup-C9ikBRw_.mjs} +9 -8
  128. package/dist/{skills-C2rTVj0n.mjs → skills-CUHIcQS6.mjs} +3 -3
  129. package/dist/{skills-CHU7uuDU.mjs → skills-CiN1OQ8W.mjs} +2 -2
  130. package/dist/snippet-B7D0uWlz.mjs +20 -0
  131. package/dist/{start-CfruN4wF.mjs → start-3PX3ahjT.mjs} +68 -37
  132. package/dist/{stash-CWuXKSZq.mjs → stash-EIDcSvpF.mjs} +17 -16
  133. package/dist/{status-D-RYZB9G.mjs → status-95ElRAu9.mjs} +12 -8
  134. package/dist/status-B0_MiZEf.mjs +100 -0
  135. package/dist/status-CEplmC44.mjs +34 -0
  136. package/dist/{stop-D8Hr4cKX.mjs → stop-CQ0XGrN8.mjs} +11 -10
  137. package/dist/{summary-Lt2XLBK9.mjs → summary-C12LiEuJ.mjs} +8 -7
  138. package/dist/{sync-schema-BDElSynU.mjs → sync-schema-Ba8M3DiX.mjs} +10 -9
  139. package/dist/{table-B-PYcgGb.mjs → table-C7a5V6Zn.mjs} +1 -1
  140. package/dist/table-e6h8SLVX.mjs +20 -0
  141. package/dist/transform-BMYh1lsC.mjs +25 -0
  142. package/dist/transform-job-Cm7z5TfH.mjs +20 -0
  143. package/dist/{transform-job-BrhOLO4M.mjs → transform-job-DeTDPMxt.mjs} +1 -1
  144. package/dist/{tree-DfvjDjmk.mjs → tree-Des2ZG9d.mjs} +6 -5
  145. package/dist/{update-CqnDMNtZ.mjs → update-Bx54nWEI.mjs} +17 -15
  146. package/dist/{update-D9Z8cL7h.mjs → update-CyIZdbIQ.mjs} +11 -10
  147. package/dist/{update-CVxOxmt6.mjs → update-DBi5U8zb.mjs} +16 -14
  148. package/dist/{update-BYduslhn.mjs → update-DHZubok3.mjs} +18 -14
  149. package/dist/{update-BgcroYkF.mjs → update-DSgceARZ.mjs} +11 -10
  150. package/dist/{update-zp7pCBZH.mjs → update-DzAN4SPj.mjs} +15 -13
  151. package/dist/{update-qnFY5IuC.mjs → update-F6DmZncY.mjs} +11 -10
  152. package/dist/{update-B0bjPqKC.mjs → update-_QfgNa53.mjs} +12 -11
  153. package/dist/{update-dashcard-CQ3kmmss.mjs → update-dashcard-wpSjv4M7.mjs} +11 -10
  154. package/dist/{update-DzgXF082.mjs → update-mYVnoYNV.mjs} +15 -13
  155. package/dist/{update-DuA8-cCq.mjs → update-njHe3j-s.mjs} +15 -13
  156. package/dist/{upgrade-CIgTr2CG.mjs → upgrade-iAuvhX-W.mjs} +9 -8
  157. package/dist/{url-B5MgZXzg.mjs → url-DWaT6WIZ.mjs} +11 -10
  158. package/dist/{uuid-CJz9TmHI.mjs → uuid-CMKnS8-z.mjs} +8 -6
  159. package/dist/{validate-CB0bu50i.mjs → validate-dPEOnOf8.mjs} +2 -1
  160. package/dist/{validate-query-CavIA0Q2.mjs → validate-query-Cw6WE5Y8.mjs} +3 -3
  161. package/dist/{values-BXN6tx1i.mjs → values-BfSTAbzc.mjs} +8 -7
  162. package/dist/verify-D5YtTqqp.mjs +79 -0
  163. package/dist/{wait-BFqBlg0y.mjs → wait-8yV9_WIo.mjs} +2 -2
  164. package/dist/{wait-tDp9ZOou.mjs → wait-Bv3Tsnv4.mjs} +12 -8
  165. package/dist/{wait-flags-CN-e9zNq.mjs → wait-flags-Dzq9BGQY.mjs} +20 -9
  166. package/dist/workspace-CKLZrR7l.mjs +26 -0
  167. package/dist/{workspace-credentials-4lIxxz4g.mjs → workspace-credentials-BXpABsNZ.mjs} +2 -2
  168. package/dist/{yaml-ECiog374.mjs → yaml-YTQiYJ9s.mjs} +1 -1
  169. package/package.json +2 -1
  170. package/skill-data/core/SKILL.md +55 -453
  171. package/skill-data/git-sync/SKILL.md +1 -1
  172. package/skill-data/mbql/SKILL.md +156 -0
  173. package/skill-data/mbql/references/operators.md +253 -0
  174. package/skill-data/transform/SKILL.md +2 -40
  175. package/skill-data/viz/SKILL.md +137 -0
  176. package/skill-data/viz/references/settings.md +312 -0
  177. package/skill-data/workspace/SKILL.md +45 -63
  178. package/skills/metabase-cli/SKILL.md +5 -26
  179. package/dist/add-collection-ucsyAMkV.mjs +0 -11
  180. package/dist/api-key-BENHbTbV.mjs +0 -13
  181. package/dist/auth-DICRtJDy.mjs +0 -19
  182. package/dist/card-l-UmrUIo.mjs +0 -20
  183. package/dist/collection-oV0olVY-.mjs +0 -19
  184. package/dist/command-augment-D9pI9Vbh.mjs +0 -11
  185. package/dist/create-CrUq6sib.mjs +0 -125
  186. package/dist/create-D3Z878yr.mjs +0 -50
  187. package/dist/dashboard-hbKDd36X.mjs +0 -20
  188. package/dist/db-qVK6NsdB.mjs +0 -22
  189. package/dist/eid-CDFXX_6H.mjs +0 -13
  190. package/dist/field-C0LE7RQI.mjs +0 -18
  191. package/dist/flag-pair-Fmcdkrfx.mjs +0 -17
  192. package/dist/get-run-CwFuR4Uw.mjs +0 -36
  193. package/dist/git-sync-DV7YjniX.mjs +0 -28
  194. package/dist/is-dirty-LxVbm2C5.mjs +0 -10
  195. package/dist/key-CCJdVWKc.mjs +0 -12
  196. package/dist/license-Cb6ewEJO.mjs +0 -17
  197. package/dist/list-DV6CONhp.mjs +0 -55
  198. package/dist/measure-XhJuL77y.mjs +0 -19
  199. package/dist/package-DFUprkSZ.mjs +0 -85
  200. package/dist/ps-Bk6unzaX.mjs +0 -11
  201. package/dist/segment-DfxZdJmR.mjs +0 -19
  202. package/dist/snippet-BCY4KHBU.mjs +0 -19
  203. package/dist/status-1oUnw803.mjs +0 -56
  204. package/dist/status-J9HIDcA5.mjs +0 -32
  205. package/dist/table-BwX3Ib5f.mjs +0 -19
  206. package/dist/transform-iaAi37V0.mjs +0 -24
  207. package/dist/transform-job-Bemonf82.mjs +0 -19
  208. package/dist/workspace-BBsT0H0g.mjs +0 -24
  209. /package/dist/{body-flags-BK7J6Daz.mjs → body-flags-D7q87Btw.mjs} +0 -0
  210. /package/dist/{field-B3gvaqpK.mjs → field-yomXlkvl.mjs} +0 -0
  211. /package/dist/{paginate-CTSfuYiF.mjs → paginate-Dfm9eO9A.mjs} +0 -0
  212. /package/dist/{revision-message-flag-oyq2xrDU.mjs → revision-message-flag-WmsIzUOM.mjs} +0 -0
  213. /package/dist/{segment-BMrUBz94.mjs → segment-Be2v4ilr.mjs} +0 -0
  214. /package/dist/{setting-CTaAeMci.mjs → setting-oL97SNeO.mjs} +0 -0
  215. /package/dist/{snippet-CSWqkslB.mjs → snippet-COggaWxx.mjs} +0 -0
  216. /package/dist/{transform-DR4ejuPM.mjs → transform-GTW3G-01.mjs} +0 -0
  217. /package/dist/{workspace-DUfqhPm5.mjs → workspace-BBXJczJK.mjs} +0 -0
@@ -8,7 +8,7 @@ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
8
8
 
9
9
  Metabase content (cards, dashboards, transforms, snippets, collections, …) can live in a git repo as YAML and round-trip in and out of a Metabase instance via the `git-sync` verbs. The instance is configured with a `remote-sync-*` settings block (URL, branch, token, type read-only/read-write); the CLI drives the sync tasks against `/api/ee/remote-sync/*`.
10
10
 
11
- This skill covers the import/export workflow. The general flag conventions and auth setup live in the `core` skill (`mb skills get core`). To author content YAML by hand, also load the `metabase-representation-format` skill — it covers the file-tree layout and per-resource YAML shape.
11
+ This skill covers the import/export workflow. The general flag conventions and auth setup live in the `core` skill (`mb skills get core`). To author content YAML by hand: the per-resource clause and settings shapes mirror the API form — query bodies follow the `mbql` skill, `visualization_settings` follow the `viz` skill — except the portable YAML uses **name-based** references (e.g. `[Sample Database, PUBLIC, ORDERS, TOTAL]`, and entity-ids for cross-entity FKs) where the API form uses numeric ids. For the on-disk folder layout, model new files on what the synced repo already contains.
12
12
 
13
13
  ## Adding / removing a directory (collection) to sync
14
14
 
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: mbql
3
+ description: Author Metabase MBQL 5 query bodies for the `mb` CLI — the only hand-authorable query format. Covers the JSON shape (lib/type mbql/query, flat stages, numeric ids), the "options object always second" clause rule, lib/uuid minting, the print-schema → dry-run → run validation loop, where MBQL 5 is consumed (mb query, card dataset_query, transform source.query, measure/segment definition), the flat-vs-legacy-envelope footgun, and naming aggregation output columns. Load whenever building or fixing an MBQL query by hand — "write an MBQL query", "create a card from MBQL", "the dataset_query is wrong", "fix the validation errors", "aggregate and group by", "order by the count", or any `--dry-run` / `mb query` work.
4
+ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
5
+ ---
6
+
7
+ # MBQL 5
8
+
9
+ MBQL 5 is the **only query format you can author by hand** with confidence — it has a bundled JSON Schema, so the CLI pre-flight-validates it before sending. Legacy MBQL 4 and native SQL are accepted but **not** schema-validated (see "Other formats" below).
10
+
11
+ Prefer MBQL over native SQL: MBQL is portable across warehouse engines and the CLI can validate it. Reach for native only when the query needs something MBQL can't express.
12
+
13
+ The general flag conventions, body-input precedence, and output flags live in the `core` skill (`mb skills get core`).
14
+
15
+ ## The shape
16
+
17
+ A query is a flat object — `lib/type`, a numeric `database` id, and an ordered `stages` array. No recursive `source-query` nesting; multi-step queries are sibling stages.
18
+
19
+ ```json
20
+ {
21
+ "lib/type": "mbql/query",
22
+ "database": 1,
23
+ "stages": [
24
+ {
25
+ "lib/type": "mbql.stage/mbql",
26
+ "source-table": 7,
27
+ "aggregation": [["count", { "lib/uuid": "<mint via mb uuid>" }]],
28
+ "breakout": [["field", { "temporal-unit": "month", "lib/uuid": "<mint>" }, 22]]
29
+ }
30
+ ]
31
+ }
32
+ ```
33
+
34
+ - **Numeric ids only.** `database`, `source-table`, and field ids are integers from `mb database list` / `mb table get <id> --include fields`. (The portable YAML representation under git-sync uses _names_ like `[Sample Database, PUBLIC, ORDERS]`; the CLI's `/api/dataset` form uses numeric ids — don't mix them.)
35
+ - **First stage** carries `source-table` (a table id) or `source-card` (a saved card). Later stages omit both and read the previous stage's output columns by name.
36
+ - `source-card` references a saved card by entity id; downstream fields are referenced by column name (string), not a field id.
37
+
38
+ ## The one rule that trips everyone: options object is **second**
39
+
40
+ Every clause is `[op, {options}, ...args]`. The options object is element **1**, args follow.
41
+
42
+ ```json
43
+ ["field", { "base-type": "type/Text", "lib/uuid": "<mint>" }, 1779] // field id is THIRD
44
+ ["count", { "lib/uuid": "<mint>" }] // no args
45
+ ["sum", { "lib/uuid": "<mint>" }, ["field", { "lib/uuid": "<mint>" }, 42]]
46
+ ["=", { "lib/uuid": "<mint>" }, ["field", { "lib/uuid": "<mint>" }, 1779], "delivered"]
47
+ ["asc", { "lib/uuid": "<mint>" }, ["field", { "lib/uuid": "<mint>" }, 42]]
48
+ ```
49
+
50
+ The legacy MBQL 4 field shape `["field", id, opts]` (id second) is **rejected** here. A slot-1 violation surfaces from `--dry-run` as `must be the field options object` / `must be the clause options object` at `/stages/0/<verb>/<n>/1`.
51
+
52
+ The same `[op, {options}, …]` rule holds for `aggregation`, `breakout` (a list of field refs), `filters` (implicitly ANDed; nest an explicit `["or", {}, …]` for OR), `order-by`, `expressions`, and join `conditions`.
53
+
54
+ ## UUIDs: mint them, never invent them
55
+
56
+ Every clause options object carries a `lib/uuid` (UUID v4). The schema enforces RFC 4122 strictly, so placeholders (`"a1"`, `"uuid-1"`, `"agg-uuid-001"`) fail pre-flight with `must be a UUID v4 (RFC 4122) — run \`mb uuid\` …`. The same applies to native template-tag ids and any other `format: "uuid"` slot.
57
+
58
+ ```bash
59
+ mb uuid --count 5 --json # → ["…","…","…","…","…"] — mint exactly what you need, in one call
60
+ ```
61
+
62
+ Workflow: count the slots (one per clause options object), `mb uuid --count <N> --json`, substitute each minted value as you build the JSON. Never copy a UUID from docs, a prior query, or another session.
63
+
64
+ **Aggregation/expression refs are the only legitimate reuse.** To reference an aggregation downstream (in `order-by` or a later stage), use `["aggregation", {options}, "<uuid>"]` where the third arg is the **string** `lib/uuid` of the target aggregation — the same minted value, by string equality. A numeric position fails with `must be the target aggregation's lib/uuid (string), not a numeric position`. Expression refs work the same way but key off the expression's name string.
65
+
66
+ ```json
67
+ "aggregation": [["count", { "lib/uuid": "AGG_UUID" }]],
68
+ "order-by": [["desc", { "lib/uuid": "ORDER_UUID" },
69
+ ["aggregation", { "lib/uuid": "REF_UUID" }, "AGG_UUID"]]]
70
+ ```
71
+
72
+ (`AGG_UUID` appears twice and must be the _same_ minted value; `ORDER_UUID` and `REF_UUID` are distinct.)
73
+
74
+ ## Authoring loop: print-schema → dry-run → run
75
+
76
+ `mb query` is the canonical authoring surface. Three modes:
77
+
78
+ ```bash
79
+ mb query --print-schema --profile <n> > /tmp/mbql-schema.json # 1. fetch the schema
80
+ mb query --file q.json --dry-run --profile <n> # 2. validate, no network
81
+ mb query --file q.json --profile <n> --json # 3. validate + run
82
+ ```
83
+
84
+ - `--print-schema` emits `{ schema, defs }` where `defs` carries `id.yaml` / `parameter.yaml` / `ref.yaml` / `temporal_bucketing.yaml` keyed by the path used in the schema's `$ref`s. Read it first for any non-trivial query — cheaper than guess-and-fail.
85
+ - `--dry-run` validates and emits `{ ok, errors: [{ path, message }] }`. Exit `0` valid, `2` invalid. No request sent. Iterate until `ok: true`.
86
+ - run (no flag) validates, then on success sends to `/api/dataset`. On validation failure it writes the same envelope, exits `2`, and **never sends**.
87
+
88
+ `path` is a JSON Pointer into the body (`/stages/0/aggregation/0`); `message` is the validator error. Exit codes: `0` valid + ran, `2` validation failed / malformed body, `1` server-side error after a valid pre-flight.
89
+
90
+ `--skip-validate` bypasses the pre-flight and sends as-is — use only when the bundled schema disagrees with what the server actually accepts (drift / false negative). Mutually exclusive with `--dry-run`. The same flag exists on `card create/update` and `transform create/update`.
91
+
92
+ ## Where MBQL 5 is consumed
93
+
94
+ The same body and the same pre-flight apply everywhere a query is embedded. Each pre-flights only when the value is MBQL 5 (`lib/type: "mbql/query"`); legacy shapes skip it; `--skip-validate` bypasses.
95
+
96
+ | Command | MBQL 5 lives at | Notes |
97
+ | --------------------------------------- | ---------------------------------------------- | ------------------------------------------- |
98
+ | `mb query` | the whole body | ad-hoc run against `/api/dataset` |
99
+ | `card create` / `card update` | `dataset_query` | a **flat** `mbql/query` — see footgun below |
100
+ | `transform create` / `transform update` | `source.query` (when `source.type` is `query`) | materializes to a warehouse table |
101
+ | `measure create` / `measure update` | `definition` | exactly one `aggregation`, no `filters` |
102
+ | `segment create` / `segment update` | `definition` | filter macro tied to a table |
103
+
104
+ ## Footgun: `dataset_query` is the flat mbql/query, not a legacy envelope
105
+
106
+ The most common mistake. The legacy MBQL 4 shape `{ "type": "query", "database": N, "query": {…} }` looks similar but is wrong for MBQL 5. `dataset_query` (and `source.query`, and `definition`) **is the `mbql/query` value itself**:
107
+
108
+ ```json
109
+ "dataset_query": {
110
+ "lib/type": "mbql/query",
111
+ "database": 2,
112
+ "stages": [{ "lib/type": "mbql.stage/mbql", "source-table": 190,
113
+ "aggregation": [["count", { "lib/uuid": "<mint>" }]] }]
114
+ }
115
+ ```
116
+
117
+ No `type:"query"` wrapper, no `query:` nesting. If you wrap MBQL 5 inside a legacy envelope the CLI rejects it pre-send with a `ConfigError` (no `--skip-validate` gets it past). If it ever reached the server it would store silently and fail at run time with `Initial MBQL stage must have either :source-table or :source-card`.
118
+
119
+ ## Other formats skip pre-flight
120
+
121
+ Anything that is not `lib/type: "mbql/query"` is sent as-is and normalized server-side:
122
+
123
+ - **Legacy MBQL 4** — `{ "type": "query", "database": N, "query": { "source-table": T, … } }`
124
+ - **Native SQL** — `{ "type": "native", "database": N, "native": { "query": "SELECT …" } }`
125
+
126
+ `mb query --file probe.json` runs these directly; `--dry-run` on them returns `{ ok: true, errors: [] }`. Don't author MBQL 4 by hand — if you need a legacy or complex query, build it in the Metabase UI and pull the body with `mb card get <id> --full --json` / `mb transform get <id> --full --json`.
127
+
128
+ ## Naming aggregation output columns
129
+
130
+ Default MBQL 5 aggregations materialize as `count`, `count_where`, `avg`, `avg_2`, `sum`, … — fine for an ad-hoc run, ugly when the output is a transform target table or a card column. Set `name` (becomes the warehouse column name) and `display-name` (the UI header) in the aggregation's options:
131
+
132
+ ```json
133
+ [
134
+ "count",
135
+ { "lib/uuid": "<mint>", "name": "shipments_shipped", "display-name": "Shipments shipped" }
136
+ ]
137
+ ```
138
+
139
+ ## Operator reference
140
+
141
+ The full operator vocabulary — filter operators (`=`, `!=`, `<`, `between`, `contains`, `is-null`, …), aggregation functions (`count`, `sum`, `avg`, `distinct`, `count-where`, `share`, …), expression operators (arithmetic, string, temporal), temporal-bucketing units, and binning strategies — lives in this skill's `references/operators.md`, in the CLI's numeric-id form. Load it on demand rather than dumping the schema:
142
+
143
+ ```bash
144
+ mb skills get mbql --full # appends references/operators.md to this body
145
+ mb skills path mbql # → the skill dir; then Read references/operators.md
146
+ ```
147
+
148
+ `mb query --print-schema` is the exhaustive-but-heavy fallback (the full JSON Schema, ~1600 lines). The cheat-sheet covers the vocabulary; the `--dry-run` loop settles any disagreement.
149
+
150
+ ## Don't
151
+
152
+ - Don't invent, hard-code, or copy `lib/uuid` values — `mb uuid` every slot at author time.
153
+ - Don't put the options object anywhere but slot 1, and don't use the legacy `["field", id, opts]` order.
154
+ - Don't wrap an MBQL 5 body in `{type:"query", query:…}` — `dataset_query` / `source.query` / `definition` is the flat `mbql/query`.
155
+ - Don't author MBQL 4 by hand — build it in the UI and pull it with `… get <id> --full --json`.
156
+ - Don't skip the `--dry-run` loop on a non-trivial query — it's free and exact.
@@ -0,0 +1,253 @@
1
+ # MBQL 5 operator reference
2
+
3
+ The complete clause vocabulary the bundled schema accepts, in the CLI's API/numeric
4
+ form. The clause _structure_ and the slot-1-options rule are in the SKILL.md body —
5
+ this file is the catalog of which operators exist and their arguments.
6
+
7
+ **Reading the tables.** Every clause is `[op, {options}, ...args]`. Below, `{…}`
8
+ abbreviates the slot-1 options object, which always carries `lib/uuid` (mint with
9
+ `mb uuid`) plus any operator-specific option noted in the row. Field refs are numeric:
10
+ `["field", {…}, <field-id>]`. Everything here passes `mb query --dry-run`; when in
11
+ doubt, that loop is the authority.
12
+
13
+ > Relative date filters use **`time-interval`** / **`relative-time-interval`** (below),
14
+ > not bare date literals. If you pulled a query from the UI that contains
15
+ > `relative-datetime` or `absolute-datetime`, the bundled schema does not yet model
16
+ > those — send it with `--skip-validate` rather than rewriting it.
17
+
18
+ ---
19
+
20
+ ## Filter operators
21
+
22
+ A stage's `filters` is a list of boolean clauses, implicitly ANDed. Nest an explicit
23
+ `["or", {…}, …]` for OR.
24
+
25
+ ### Logical
26
+
27
+ | Op | Args | Notes |
28
+ | ----- | ------------------ | ----------------------------------------------------- |
29
+ | `and` | 2+ boolean clauses | Logical AND (usually implicit via the `filters` list) |
30
+ | `or` | 2+ boolean clauses | Logical OR |
31
+ | `not` | 1 boolean clause | Logical NOT |
32
+
33
+ ### Comparison
34
+
35
+ | Op | Args | Notes |
36
+ | ----------------- | -------------------------------------------------------- | ----------------------- |
37
+ | `=` | field, 1+ values | Multi-value = IN |
38
+ | `!=` | field, 1+ values | Multi-value = NOT IN |
39
+ | `<` `>` `<=` `>=` | 2 orderable | |
40
+ | `between` | field, min, max | Inclusive |
41
+ | `inside` | lat-field, lon-field, lat-max, lon-min, lat-min, lon-max | Geographic bounding box |
42
+
43
+ ```json
44
+ ["between", {…}, ["field", {…}, 12], 10, 100]
45
+ ["=", {…}, ["field", {…}, 7], "Widget", "Gadget"]
46
+ ```
47
+
48
+ ### Null / empty
49
+
50
+ | Op | Args | Notes |
51
+ | ----------- | ------------------- | --------------------- |
52
+ | `is-null` | 1 expression | |
53
+ | `not-null` | 1 expression | |
54
+ | `is-empty` | 1 string expression | NULL or `""` |
55
+ | `not-empty` | 1 string expression | not NULL and not `""` |
56
+
57
+ ### String match
58
+
59
+ N-ary (multiple values OR'd). Accept a `case-sensitive` option (default `true`) in the
60
+ options object.
61
+
62
+ | Op | Args |
63
+ | ------------------ | ----------------- |
64
+ | `contains` | field, 1+ strings |
65
+ | `does-not-contain` | field, 1+ strings |
66
+ | `starts-with` | field, 1+ strings |
67
+ | `ends-with` | field, 1+ strings |
68
+
69
+ ```json
70
+ ["contains", { "lib/uuid": "<mint>", "case-sensitive": false }, ["field", {…}, 9], "widget"]
71
+ ```
72
+
73
+ ### Temporal
74
+
75
+ | Op | Args | Notes |
76
+ | ------------------------ | ---------------------------------------------------------- | --------------------------------------------------- |
77
+ | `time-interval` | temporal-field, n, unit | `n` = integer, or `"current"` / `"last"` / `"next"` |
78
+ | `relative-time-interval` | temporal-field, value, bucket, offset-value, offset-bucket | interval with offset |
79
+
80
+ Units (truncation only): `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
81
+ `month`, `quarter`, `year`.
82
+
83
+ ```json
84
+ ["time-interval", {…}, ["field", {…}, 22], -30, "day"] // last 30 days
85
+ ["time-interval", {…}, ["field", {…}, 22], "current", "month"]
86
+ ```
87
+
88
+ ### Segment
89
+
90
+ | Op | Args | Notes |
91
+ | --------- | ---------- | ------------------------- |
92
+ | `segment` | segment id | Reference a saved segment |
93
+
94
+ ---
95
+
96
+ ## Aggregation functions
97
+
98
+ A stage's `aggregation` is a list of aggregation clauses.
99
+
100
+ | Op | Args | Notes |
101
+ | ----------------------- | -------------------------- | -------------------------------- |
102
+ | `count` | none, or 1 expression | with arg: count non-NULL |
103
+ | `sum` `avg` `min` `max` | 1 numeric/orderable | |
104
+ | `distinct` | 1 expression | count of distinct values |
105
+ | `cum-count` | none or 1 expression | running count |
106
+ | `cum-sum` | 1 numeric | running sum |
107
+ | `stddev` `var` `median` | 1 numeric | |
108
+ | `percentile` | numeric, p (0.0–1.0) | |
109
+ | `count-where` | 1 boolean clause | |
110
+ | `sum-where` | numeric, boolean clause | |
111
+ | `distinct-where` | expression, boolean clause | |
112
+ | `share` | 1 boolean clause | proportion 0–1 |
113
+ | `metric` | metric id | reference a saved metric card |
114
+ | `measure` | measure id | reference a saved measure (v59+) |
115
+
116
+ ```json
117
+ ["count", {…}]
118
+ ["sum", {…}, ["field", {…}, 42]]
119
+ ["count-where", {…}, [">", {…}, ["field", {…}, 42], 100]]
120
+ ```
121
+
122
+ **Naming** — set `name` (warehouse column) and/or `display-name` (UI header) in the
123
+ options object: `["sum", { "lib/uuid": "<mint>", "name": "revenue", "display-name": "Revenue" }, …]`.
124
+
125
+ **Window function** — `offset` is only valid inside `aggregation`:
126
+
127
+ | Op | Args | Notes |
128
+ | -------- | ------------- | ------------------------------------------------- |
129
+ | `offset` | expression, n | value n rows before (negative) / after (positive) |
130
+
131
+ ---
132
+
133
+ ## Expression operators
134
+
135
+ Used in `expressions` (named, via `lib/expression-name` in options) and inline.
136
+
137
+ ### Arithmetic
138
+
139
+ | Op | Args | Notes |
140
+ | --- | ---------------------------------- | -------------------- |
141
+ | `+` | 2+ numeric, or temporal + interval | |
142
+ | `-` | 1+ numeric, or temporal − interval | unary = negation |
143
+ | `*` | 2+ numeric | |
144
+ | `/` | 2+ numeric | always returns float |
145
+
146
+ ### Math
147
+
148
+ | Op | Args |
149
+ | ---------------------------- | -------------- |
150
+ | `abs` `ceil` `floor` `round` | 1 numeric |
151
+ | `power` | base, exponent |
152
+ | `sqrt` `exp` `log` | 1 numeric |
153
+
154
+ ### String
155
+
156
+ | Op | Args |
157
+ | ---------------------------------- | ------------------------------- |
158
+ | `concat` | 2+ expressions |
159
+ | `substring` | str, start (1-indexed), length? |
160
+ | `replace` | str, find, replace |
161
+ | `regex-match-first` | str, regex |
162
+ | `split-part` | str, delimiter, position |
163
+ | `trim` `ltrim` `rtrim` | 1 string |
164
+ | `upper` `lower` | 1 string |
165
+ | `length` | 1 string |
166
+ | `host` `domain` `subdomain` `path` | 1 URL string |
167
+
168
+ ### Temporal
169
+
170
+ | Op | Args | Notes |
171
+ | ----------------------------------------------------------------------------------- | ------------------------------- | -------------------------- |
172
+ | `now` | none | datetime |
173
+ | `today` | none | date |
174
+ | `interval` | amount, unit | a temporal interval |
175
+ | `datetime-add` | temporal, amount, unit | |
176
+ | `datetime-subtract` | temporal, amount, unit | |
177
+ | `datetime-diff` | datetime1, datetime2, unit | |
178
+ | `convert-timezone` | temporal, target-tz, source-tz? | |
179
+ | `get-year` `get-quarter` `get-month` `get-day` `get-hour` `get-minute` `get-second` | 1 temporal | integer component |
180
+ | `get-day-of-week` | temporal, mode? | mode `iso`/`us`/`instance` |
181
+ | `get-week` | temporal, mode? | mode `iso`/`us`/`instance` |
182
+ | `temporal-extract` | temporal, unit, mode? | generic extraction |
183
+ | `month-name` `quarter-name` `day-name` | 1 integer | name from number |
184
+
185
+ Add/subtract/interval units: `year`, `quarter`, `month`, `week`, `day`, `hour`,
186
+ `minute`, `second`, `millisecond`. `datetime-diff` units: same minus `millisecond`.
187
+ `temporal-extract` units: `year-of-era`, `quarter-of-year`, `month-of-year`,
188
+ `week-of-year-iso`, `week-of-year-us`, `week-of-year-instance`, `day-of-month`,
189
+ `day-of-week`, `day-of-week-iso`, `hour-of-day`, `minute-of-hour`, `second-of-minute`.
190
+
191
+ ### Type conversion
192
+
193
+ | Op | Args |
194
+ | --------- | ----------------- |
195
+ | `integer` | string or numeric |
196
+ | `float` | string |
197
+ | `text` | 1 expression |
198
+
199
+ ### Conditional
200
+
201
+ | Op | Args | Notes |
202
+ | ---------- | ------------------------------ | ---------------------------------------------------- |
203
+ | `case` | `[[cond, value], …]`, default? | if/then/else; default is the trailing positional arg |
204
+ | `if` | same as `case` | alias for `case` |
205
+ | `coalesce` | 2+ expressions | first non-null |
206
+
207
+ ```json
208
+ ["case", { "lib/uuid": "<mint>", "lib/expression-name": "Tier" },
209
+ [[[">", {…}, ["field", {…}, 14], 100], "Premium"],
210
+ [["<=", {…}, ["field", {…}, 14], 20], "Budget"]],
211
+ "Standard"]
212
+ ```
213
+
214
+ (`case`'s first arg is a list of `[condition, value]` pairs; the optional 4th
215
+ positional arg is the default.)
216
+
217
+ ---
218
+
219
+ ## References (within clauses)
220
+
221
+ | Ref | Shape | Notes |
222
+ | ----------- | ------------------------------------ | ------------------------------------------------------------------------------ |
223
+ | field | `["field", {…}, <field-id>]` | numeric id; options may carry `base-type`, `temporal-unit`, `binning` |
224
+ | expression | `["expression", {…}, "<name>"]` | by the expression's `lib/expression-name` string |
225
+ | aggregation | `["aggregation", {…}, "<agg-uuid>"]` | 3rd arg is the **string** `lib/uuid` of the target aggregation, not a position |
226
+
227
+ ## Field option: temporal bucketing
228
+
229
+ `temporal-unit` in a field ref's options buckets a datetime.
230
+
231
+ - Truncation: `default`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
232
+ `month`, `quarter`, `year`.
233
+ - Extraction (returns an integer): `minute-of-hour`, `hour-of-day`, `day-of-week`,
234
+ `day-of-week-iso`, `day-of-month`, `day-of-year`, `week-of-year`, `week-of-year-iso`,
235
+ `month-of-year`, `quarter-of-year`, `year-of-era`, `second-of-minute`.
236
+
237
+ ```json
238
+ ["field", { "lib/uuid": "<mint>", "temporal-unit": "month" }, 22]
239
+ ```
240
+
241
+ ## Field option: binning
242
+
243
+ `binning` in a field ref's options groups a numeric/coordinate column.
244
+
245
+ | `strategy` | Extra property | Notes |
246
+ | ----------- | -------------------- | -------------------------------- |
247
+ | `num-bins` | `num-bins` (integer) | fixed number of equal-width bins |
248
+ | `bin-width` | `bin-width` (number) | fixed bin width |
249
+ | `default` | — | Metabase chooses |
250
+
251
+ ```json
252
+ ["field", { "lib/uuid": "<mint>", "binning": { "strategy": "num-bins", "num-bins": 10 } }, 14]
253
+ ```
@@ -17,47 +17,9 @@ A transform has two halves:
17
17
  - `source` — the query to run (`type: "query"`, with `query.type` of `native` or `mbql`).
18
18
  - `target` — the warehouse destination (`type: "table"`, with `database`, `schema`, `name`).
19
19
 
20
- Native SQL is the simplest source and the easiest to author by hand. MBQL is what the Metabase UI emits and is much more verbose; pull a sample with `mb transform get <id> --full --json` if you need its shape.
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
21
 
22
- If `source.query` is **MBQL 5** (`lib/type: "mbql/query"`), `transform create` and `transform update` validate it against the bundled query schema before sending; failure exits 2 with `{ ok, errors: [{path, message}] }` on stdout. To author MBQL 5 by hand: fetch the schema via `mb query --print-schema --profile <n>`, iterate the body with `mb query --file q.json --dry-run --profile <n>` until `ok: true`, then drop it into `source.query`. Legacy MBQL 4 and native sources skip pre-flight. Pass `--skip-validate` to bypass the pre-flight and let the server be the authorityuseful when the bundled schema disagrees with what the server actually accepts.
23
-
24
- **Mint UUIDs for `lib/uuid` slots before assembling the body — never invent, hard-code, or reuse them.** Every clause options object carries a `lib/uuid` (UUID v4); the bundled schema enforces RFC 4122 format strictly, so placeholder strings fail `--dry-run`. Workflow: count the slots, run `mb uuid --count <N> --json`, substitute each minted value into its slot. The examples below use `<UUID:label>` sentinels (NOT valid UUIDs) so the assembly step is unambiguous — replace each sentinel with a freshly-minted UUID before sending. Same `<UUID:label>` token must be replaced with the same minted UUID (used for aggregation-ref ↔ aggregation pairing); distinct sentinels get distinct UUIDs.
25
-
26
- **Clause shape: opts always second, args after.** Every clause is `[op, {options}, ...args]`. Field refs are `["field", {options}, fieldId]` (id third), not the legacy MBQL 4 shape `["field", id, opts]`. The same rule holds for aggregations, filters, order-by — the options object never moves out of slot 1.
27
-
28
- ## MBQL 5 aggregations: name your output columns
29
-
30
- Default MBQL 5 aggregations materialize as `count`, `count_where`, `count_where_2`, `avg`, `avg_2`, `sum`, … — ugly when the result is a transform target. Pass `name` and `display-name` in the aggregation's options object to control them. Mint 4 UUIDs (`mb uuid --count 4 --json`) for the slots below before assembling:
31
-
32
- ```json
33
- ["count",
34
- {"lib/uuid": "<UUID:agg-shipped>", "name": "shipments_shipped", "display-name": "Shipments shipped"}]
35
-
36
- ["count-where",
37
- {"lib/uuid": "<UUID:agg-delivered>", "name": "shipments_delivered", "display-name": "Shipments delivered"},
38
- ["=", {"lib/uuid": "<UUID:eq-filter>"},
39
- ["field", {"base-type": "type/Text", "lib/uuid": "<UUID:status-field>"}, 1779],
40
- "delivered"]]
41
- ```
42
-
43
- The `name` value becomes the warehouse column name on the materialized table. The `display-name` is the column header in the UI.
44
-
45
- ## MBQL 5 order-by referencing an aggregation
46
-
47
- Order by an aggregation column with an `["aggregation", {…}, "<aggregation-uuid>"]` ref — the third arg is the **string UUID** of the target aggregation's `lib/uuid`, **not** its numeric position. The aggregation's own `lib/uuid` and the ref's third element must be the same minted UUID (string equality); the order-by clause itself and the ref clause each carry their own separate `lib/uuid` in their options. Mint 3 UUIDs and substitute — note that `<UUID:agg-count>` appears twice and gets the same minted value:
48
-
49
- ```json
50
- "aggregation": [
51
- ["count", {"lib/uuid": "<UUID:agg-count>"}]
52
- ],
53
- "order-by": [
54
- ["desc", {"lib/uuid": "<UUID:order-desc>"},
55
- ["aggregation", {"lib/uuid": "<UUID:agg-ref>"},
56
- "<UUID:agg-count>"]]
57
- ]
58
- ```
59
-
60
- A numeric index (`["aggregation", {…}, 0]`) fails pre-flight with `must be the target aggregation's lib/uuid (string), not a numeric position` at `/stages/0/order-by/0/2/2`.
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 usuala bare `count` / `avg_2` becomes the warehouse column name; see the `mbql` skill's "Naming aggregation output columns".
61
23
 
62
24
  ## Create + run (native SQL)
63
25
 
@@ -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.