@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.
Files changed (218) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/README.md +147 -101
  3. package/dist/{add-collection-CffaBB-Y.mjs → add-collection-BU8r3r2M.mjs} +9 -4
  4. package/dist/add-collection-C0w6ACQF.mjs +11 -0
  5. package/dist/{archive-uJrslh9r.mjs → archive-BNinrUak.mjs} +9 -8
  6. package/dist/{archive-GdGm7l2e.mjs → archive-C1enZgKV.mjs} +8 -7
  7. package/dist/archive-CDA0KxL8.mjs +40 -0
  8. package/dist/{archive-BAcEXbT9.mjs → archive-CRhiBpPJ.mjs} +9 -8
  9. package/dist/{archive-B_B3MQp0.mjs → archive-DMPS8Kih.mjs} +9 -8
  10. package/dist/archive-lWgqiFAt.mjs +40 -0
  11. package/dist/auth-CzXb_zB2.mjs +19 -0
  12. package/dist/{body-D6dHGjMT.mjs → body-DjdFxjpg.mjs} +4 -4
  13. package/dist/{branches-Bpe40fEd.mjs → branches-B1WRfG7-.mjs} +11 -7
  14. package/dist/{cancel-BWTY6oYI.mjs → cancel-Dl_Ho056.mjs} +7 -6
  15. package/dist/{cancel-task--BfiAXfS.mjs → cancel-task-CdigdCaO.mjs} +11 -7
  16. package/dist/capabilities-7e9MgquN.mjs +29 -0
  17. package/dist/card-DP4rfoOi.mjs +21 -0
  18. package/dist/{card-CQxvHeyP.mjs → card-DlCAaAPq.mjs} +1 -1
  19. package/dist/{cards-CVlFJxYh.mjs → cards-BGiJS675.mjs} +8 -7
  20. package/dist/cli.mjs +267 -44
  21. package/dist/collection-tY18ezvn.mjs +21 -0
  22. package/dist/{predicates-CGO17Q15.mjs → command-augment-BH9qgQ5u.mjs} +66 -14
  23. package/dist/create-BNiva__H.mjs +52 -0
  24. package/dist/{create-izE3EKCt.mjs → create-BTcpaop_.mjs} +9 -8
  25. package/dist/{create-BykvNpSA.mjs → create-BYlIju0b.mjs} +14 -12
  26. package/dist/{create-Bu-YhIDL.mjs → create-Be_0Vier.mjs} +10 -9
  27. package/dist/{create-DYoc9IXW.mjs → create-CHF313Qg.mjs} +13 -9
  28. package/dist/{create-Cz3_Wxdt.mjs → create-CwGtmwqm.mjs} +14 -12
  29. package/dist/{create-DP8RrLDi.mjs → create-CzzrbL0u.mjs} +10 -9
  30. package/dist/{create-BzElku2l.mjs → create-DGth_uOp.mjs} +14 -12
  31. package/dist/{create-branch-B49UQyCK.mjs → create-branch-DKZkoQ64.mjs} +11 -7
  32. package/dist/{create-DQVdMT2Y.mjs → create-dhxPxfF3.mjs} +16 -14
  33. package/dist/{credentials-xKSoP6eh.mjs → credentials-dzeq7ckm.mjs} +12 -10
  34. package/dist/{current-task-DweHmjlk.mjs → current-task-CCRzm0_7.mjs} +11 -7
  35. package/dist/dashboard-ChM_Tu0l.mjs +22 -0
  36. package/dist/{dashboard-CnMD04PQ.mjs → dashboard-FY5UzJ_Z.mjs} +2 -1
  37. package/dist/{database-BNlvldUL.mjs → database-CIXwHKjK.mjs} +3 -3
  38. package/dist/{database-vvig8k4x.mjs → database-lH-B3G1I.mjs} +1 -1
  39. package/dist/db-DrQn_i3W.mjs +22 -0
  40. package/dist/{remove-B3ZEqBF7.mjs → delete-CM3jnAeQ.mjs} +21 -20
  41. package/dist/{delete-DojHmKeM.mjs → delete-Dimc-2y8.mjs} +9 -8
  42. package/dist/{delete-DIz9Tgz5.mjs → delete-ZjnV35OJ.mjs} +9 -8
  43. package/dist/{delete-runtime-BkAdygbs.mjs → delete-runtime-B6RQo_pw.mjs} +5 -3
  44. package/dist/{delete-table-DjN8E3sd.mjs → delete-table-agZJpivt.mjs} +9 -8
  45. package/dist/{deprovision-_HDcBApz.mjs → deprovision-CwxcIT3k.mjs} +16 -12
  46. package/dist/{dirty-Co8V0SZ3.mjs → dirty-D4d0yHqj.mjs} +11 -7
  47. package/dist/{docker-D9sC_37H.mjs → docker-Oq80q3tu.mjs} +4 -4
  48. package/dist/{translate-CG_Ka0dO.mjs → eid-BXzaQh0o.mjs} +37 -22
  49. package/dist/error-C9S6PN3-.mjs +190 -0
  50. package/dist/{export-CVMFxoo1.mjs → export-DTygoXBP.mjs} +17 -16
  51. package/dist/field-Z6Pcxf4n.mjs +19 -0
  52. package/dist/{fields-Coha7vKv.mjs → fields-CoQi99gv.mjs} +9 -8
  53. package/dist/{get-DXv2FkA7.mjs → get-Bzys7vgp.mjs} +8 -7
  54. package/dist/{get-bNtA7vWe.mjs → get-C2p383Qc.mjs} +8 -7
  55. package/dist/{get-Br6WayZv.mjs → get-C3HdQ91a.mjs} +8 -7
  56. package/dist/{get-BOtKerj8.mjs → get-CP3Z3NiH.mjs} +9 -8
  57. package/dist/{get-BSKoL8ek.mjs → get-C_w1kvN3.mjs} +9 -8
  58. package/dist/{get-Be6EFh94.mjs → get-CzuzeKSe.mjs} +10 -9
  59. package/dist/{get-BVTz9B_H.mjs → get-D3SbEQSE.mjs} +10 -9
  60. package/dist/get-DFxZXaKz.mjs +79 -0
  61. package/dist/{get-DZrV7v9d.mjs → get-DQTZG_NP.mjs} +8 -7
  62. package/dist/{get-CJwzbVjc.mjs → get-DSWFjy7O.mjs} +8 -7
  63. package/dist/{get-BxzCKVC6.mjs → get-Ddr0XLh7.mjs} +8 -7
  64. package/dist/{get-AOvWo48B.mjs → get-Hc93A0Yz.mjs} +8 -7
  65. package/dist/{get-C_6K7MSW.mjs → get-lb7q3JYs.mjs} +7 -6
  66. package/dist/get-run-B7sKdaDU.mjs +38 -0
  67. package/dist/git-sync-CiGAad76.mjs +28 -0
  68. package/dist/{has-remote-changes-D6xgsuUr.mjs → has-remote-changes-BY10-nnE.mjs} +11 -7
  69. package/dist/{import-Dv0ORSNw.mjs → import-CiMz4Wz-.mjs} +17 -16
  70. package/dist/{input-BQ-BZA8h.mjs → input-cMSEqISy.mjs} +7 -4
  71. package/dist/{is-dirty-WNi8a6O9.mjs → is-dirty-BZOaryxT.mjs} +9 -4
  72. package/dist/is-dirty-Ume4oV0j.mjs +10 -0
  73. package/dist/{items-CTcAMknV.mjs → items-BWfvkY-J.mjs} +9 -8
  74. package/dist/key-C2XG394c.mjs +17 -0
  75. package/dist/license-Dxarh-gG.mjs +17 -0
  76. package/dist/{list-FXuSCYpa.mjs → list--OYdUTtu.mjs} +7 -6
  77. package/dist/{list-8oVMvlLV.mjs → list-2j7GsXsl.mjs} +7 -6
  78. package/dist/{list-xQmtQPSl.mjs → list-BI4zr8LW.mjs} +10 -8
  79. package/dist/{list-DhWG5jiW.mjs → list-Brgh-Z2v.mjs} +8 -6
  80. package/dist/{list-DSs0Q78i.mjs → list-C3hfovHv.mjs} +7 -6
  81. package/dist/{list-DvUjMQze.mjs → list-CL7eCOQE.mjs} +7 -6
  82. package/dist/list-Clz5igWg.mjs +44 -0
  83. package/dist/list-D4sFiqX8.mjs +173 -0
  84. package/dist/{list-BxdXvGTK.mjs → list-DXH7TlkU.mjs} +9 -7
  85. package/dist/{list-CocYwmnI.mjs → list-DZ8fNUoQ.mjs} +9 -8
  86. package/dist/{list-DjhZU-FY.mjs → list-SOG0whQ-.mjs} +7 -6
  87. package/dist/{list-DI7K3K6k.mjs → list-d58BprgJ.mjs} +7 -6
  88. package/dist/{list-NiwCL_1X.mjs → list-sD5N3fGk.mjs} +9 -8
  89. package/dist/{list-CbJeP0Z6.mjs → list-zSO0DMw-.mjs} +10 -6
  90. package/dist/{login-SXsSH0I1.mjs → login-Bm2AnCez.mjs} +65 -80
  91. package/dist/{logout-bgOXjxbN.mjs → logout-BlyRJODO.mjs} +8 -7
  92. package/dist/{logs-BnwVbFuD.mjs → logs-CywPikkL.mjs} +9 -8
  93. package/dist/{manifest-CGM7XNLC.mjs → manifest-BBR46KFM.mjs} +15 -15
  94. package/dist/measure-C44EK_xt.mjs +20 -0
  95. package/dist/{measure-BEQfnLdN.mjs → measure-ClESGxIb.mjs} +2 -2
  96. package/dist/{metadata-Bu2HOmuX.mjs → metadata-B8ZSF9LA.mjs} +10 -9
  97. package/dist/{metadata-B0WZT3Yb.mjs → metadata-DqiI2q9q.mjs} +9 -8
  98. package/dist/parse-enum-CrEWOhuY.mjs +11 -0
  99. package/dist/{parse-id-B3B-0hUA.mjs → parse-id-lk_K-CEF.mjs} +1 -1
  100. package/dist/{parse-ref-D1yeDOn8.mjs → parse-ref-BiETXmvm.mjs} +1 -1
  101. package/dist/{parse-schemas-DgtVLikM.mjs → parse-schemas-BqUdWUwq.mjs} +2 -2
  102. package/dist/path-AEtZ3mBq.mjs +58 -0
  103. package/dist/{poll-BCnrcUVf.mjs → poll-DHKDpCiq.mjs} +2 -2
  104. package/dist/{poll-task-0b1V6G-8.mjs → poll-task-Cooi0lQV.mjs} +3 -20
  105. package/dist/{preflight-5ACaYnDp.mjs → preflight-aXV5LyDs.mjs} +4 -4
  106. package/dist/{process-FjsqDwKo.mjs → process-C7V8LJ-j.mjs} +1 -1
  107. package/dist/{prompt-DgDNy_Pc.mjs → prompt-CFKoys7k.mjs} +3 -1
  108. package/dist/{provision-29Zt62Ft.mjs → provision-UWcNDoDe.mjs} +29 -24
  109. package/dist/{ps-BMFiRCi4.mjs → ps-CJU0EbrC.mjs} +5 -3
  110. package/dist/ps-DEroLgbI.mjs +11 -0
  111. package/dist/{query-DxA353Hy.mjs → query-AaKzYnTY.mjs} +9 -8
  112. package/dist/{query-aba8MEe_.mjs → query-BlsVNZpD.mjs} +15 -13
  113. package/dist/{remove-BfgU_CQi.mjs → remove-BFWun0e8.mjs} +9 -8
  114. package/dist/{remove-collection-Brv72xUe.mjs → remove-collection-CoCmrrQs.mjs} +13 -9
  115. package/dist/{render-DuoDUTVL.mjs → render-CfznwleY.mjs} +15 -17
  116. package/dist/render-OQn3iRsI.mjs +32 -0
  117. package/dist/{rescan-values-DIAdjoq7.mjs → rescan-values-C0FDsjT7.mjs} +10 -9
  118. package/dist/{run-CgXRo0hD.mjs → run-B4Wn43zm.mjs} +10 -9
  119. package/dist/{runs-DtLRw6xg.mjs → runs-Bbaszr18.mjs} +9 -8
  120. package/dist/{runtime-Br8L4NPm.mjs → runtime-Dmv5VtUK.mjs} +657 -428
  121. package/dist/{schema-tables-DiKMY6lx.mjs → schema-tables-CaWinbuK.mjs} +9 -8
  122. package/dist/{schemas-Bvr8cOzo.mjs → schemas-DUgGpAyB.mjs} +7 -6
  123. package/dist/{search-BT_TCcTd.mjs → search-BLrBXLUk.mjs} +12 -16
  124. package/dist/segment-B3Uwwcsm.mjs +20 -0
  125. package/dist/{set-DtG0KH6P.mjs → set-B8cUbRLD.mjs} +13 -12
  126. package/dist/{set-CAIkXlPy.mjs → set-DfGsta5O.mjs} +11 -10
  127. package/dist/{setting-BDOi5fk_.mjs → setting-D2p2MA7f.mjs} +3 -3
  128. package/dist/{setup-LjTvvlJy.mjs → setup-C9ikBRw_.mjs} +9 -8
  129. package/dist/skills-CUHIcQS6.mjs +18 -0
  130. package/dist/skills-CiN1OQ8W.mjs +191 -0
  131. package/dist/snippet-B7D0uWlz.mjs +20 -0
  132. package/dist/{start-CXKt0Q7A.mjs → start-3PX3ahjT.mjs} +68 -36
  133. package/dist/{stash-dRw1UEwg.mjs → stash-EIDcSvpF.mjs} +17 -16
  134. package/dist/{status-C2niMfrQ.mjs → status-95ElRAu9.mjs} +12 -8
  135. package/dist/status-B0_MiZEf.mjs +100 -0
  136. package/dist/status-CEplmC44.mjs +34 -0
  137. package/dist/{stop-BdedYfwU.mjs → stop-CQ0XGrN8.mjs} +11 -10
  138. package/dist/{summary-BPDA4K99.mjs → summary-C12LiEuJ.mjs} +8 -7
  139. package/dist/{sync-schema-D95LLRpf.mjs → sync-schema-Ba8M3DiX.mjs} +10 -9
  140. package/dist/{table-B-PYcgGb.mjs → table-C7a5V6Zn.mjs} +1 -1
  141. package/dist/table-e6h8SLVX.mjs +20 -0
  142. package/dist/transform-BMYh1lsC.mjs +25 -0
  143. package/dist/transform-job-Cm7z5TfH.mjs +20 -0
  144. package/dist/{transform-job-Csr86muI.mjs → transform-job-DeTDPMxt.mjs} +1 -1
  145. package/dist/{tree-DazZT7dR.mjs → tree-Des2ZG9d.mjs} +6 -5
  146. package/dist/{update-DE6kjV-f.mjs → update-Bx54nWEI.mjs} +17 -15
  147. package/dist/{update-bW-i6gjZ.mjs → update-CyIZdbIQ.mjs} +11 -10
  148. package/dist/{update-djgvzO3K.mjs → update-DBi5U8zb.mjs} +16 -14
  149. package/dist/{update-CJSDB6S8.mjs → update-DHZubok3.mjs} +18 -14
  150. package/dist/{update-BBfvArCx.mjs → update-DSgceARZ.mjs} +11 -10
  151. package/dist/{update-DSWZSfpw.mjs → update-DzAN4SPj.mjs} +15 -13
  152. package/dist/{update-WyRKlQPh.mjs → update-F6DmZncY.mjs} +11 -10
  153. package/dist/{update-DTIWJxob.mjs → update-_QfgNa53.mjs} +12 -11
  154. package/dist/{update-dashcard-BhD5x__K.mjs → update-dashcard-wpSjv4M7.mjs} +11 -10
  155. package/dist/{update-9kVyE3BJ.mjs → update-mYVnoYNV.mjs} +15 -13
  156. package/dist/{update-659eQR1L.mjs → update-njHe3j-s.mjs} +15 -13
  157. package/dist/{upgrade-D58rvXHM.mjs → upgrade-iAuvhX-W.mjs} +9 -8
  158. package/dist/{url-DKkSu2D8.mjs → url-DWaT6WIZ.mjs} +11 -10
  159. package/dist/{uuid-BF20B59s.mjs → uuid-CMKnS8-z.mjs} +8 -6
  160. package/dist/{validate-CB0bu50i.mjs → validate-dPEOnOf8.mjs} +2 -1
  161. package/dist/{validate-query-CavIA0Q2.mjs → validate-query-Cw6WE5Y8.mjs} +3 -3
  162. package/dist/{values-DyjmpcbT.mjs → values-BfSTAbzc.mjs} +8 -7
  163. package/dist/verify-D5YtTqqp.mjs +79 -0
  164. package/dist/{wait-CeUPCgdc.mjs → wait-8yV9_WIo.mjs} +2 -2
  165. package/dist/{wait-DhkTaV6E.mjs → wait-Bv3Tsnv4.mjs} +12 -8
  166. package/dist/{wait-flags-BR-yqe7y.mjs → wait-flags-Dzq9BGQY.mjs} +20 -9
  167. package/dist/workspace-CKLZrR7l.mjs +26 -0
  168. package/dist/{workspace-credentials-Cctumbru.mjs → workspace-credentials-BXpABsNZ.mjs} +2 -41
  169. package/dist/yaml-YTQiYJ9s.mjs +43 -0
  170. package/package.json +6 -2
  171. package/skill-data/core/SKILL.md +177 -0
  172. package/skill-data/git-sync/SKILL.md +196 -0
  173. package/skill-data/mbql/SKILL.md +156 -0
  174. package/skill-data/mbql/references/operators.md +253 -0
  175. package/skill-data/transform/SKILL.md +197 -0
  176. package/skill-data/viz/SKILL.md +137 -0
  177. package/skill-data/viz/references/settings.md +312 -0
  178. package/skill-data/workspace/SKILL.md +390 -0
  179. package/skills/metabase-cli/SKILL.md +21 -0
  180. package/dist/add-collection-CPL1njYZ.mjs +0 -11
  181. package/dist/api-key-9p1UPnXn.mjs +0 -13
  182. package/dist/auth-N4w5xtwW.mjs +0 -19
  183. package/dist/card-4rZRb5bc.mjs +0 -20
  184. package/dist/collection-Cp_B02I4.mjs +0 -19
  185. package/dist/command-augment-D9pI9Vbh.mjs +0 -11
  186. package/dist/create-doyv3SxU.mjs +0 -50
  187. package/dist/create-ov-De5dO.mjs +0 -125
  188. package/dist/dashboard-BYBiA-IG.mjs +0 -20
  189. package/dist/db-CObVU22j.mjs +0 -22
  190. package/dist/eid-Cr5r-t9B.mjs +0 -13
  191. package/dist/field-CbljasCH.mjs +0 -18
  192. package/dist/flag-pair-Fmcdkrfx.mjs +0 -17
  193. package/dist/get-run-CSrXHDGS.mjs +0 -36
  194. package/dist/git-sync-BGkS8o5b.mjs +0 -28
  195. package/dist/is-dirty-BOZ4xz92.mjs +0 -10
  196. package/dist/key-CCJdVWKc.mjs +0 -12
  197. package/dist/license-DLLTpFvP.mjs +0 -17
  198. package/dist/list-BNzdnE1c.mjs +0 -55
  199. package/dist/measure-B54VtKym.mjs +0 -19
  200. package/dist/package-D-aVYFKM.mjs +0 -80
  201. package/dist/ps-C5FOLwL2.mjs +0 -11
  202. package/dist/segment-C2ui5dSd.mjs +0 -19
  203. package/dist/snippet-BcgVYsoR.mjs +0 -19
  204. package/dist/status-BEONmJWv.mjs +0 -32
  205. package/dist/status-BWep0PFe.mjs +0 -56
  206. package/dist/table-lCNGbvej.mjs +0 -19
  207. package/dist/transform-BGAm1s4f.mjs +0 -24
  208. package/dist/transform-job-cNTJ30pm.mjs +0 -19
  209. package/dist/workspace-DtcBldk0.mjs +0 -24
  210. /package/dist/{body-flags-BK7J6Daz.mjs → body-flags-D7q87Btw.mjs} +0 -0
  211. /package/dist/{field-B3gvaqpK.mjs → field-yomXlkvl.mjs} +0 -0
  212. /package/dist/{paginate-CTSfuYiF.mjs → paginate-Dfm9eO9A.mjs} +0 -0
  213. /package/dist/{revision-message-flag-oyq2xrDU.mjs → revision-message-flag-WmsIzUOM.mjs} +0 -0
  214. /package/dist/{segment-BMrUBz94.mjs → segment-Be2v4ilr.mjs} +0 -0
  215. /package/dist/{setting-CTaAeMci.mjs → setting-oL97SNeO.mjs} +0 -0
  216. /package/dist/{snippet-Dw0Sjzkr.mjs → snippet-COggaWxx.mjs} +0 -0
  217. /package/dist/{transform-IEX4Mx3X.mjs → transform-GTW3G-01.mjs} +0 -0
  218. /package/dist/{workspace-C5q4nbpY.mjs → workspace-BBXJczJK.mjs} +0 -0
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: git-sync
3
+ description: Round-trip Metabase content (cards, dashboards, transforms, snippets, collections) between an instance and a git remote via `mb git-sync …` — status, dirty / has-remote-changes checks, import (with first-fresh-workspace exception), export (with branch guard + working-tree drift), branches, stash, add/remove a collection from sync. Load when the user wants to "import the latest changes", "export to git", "git sync", "dirty check", "stash before pulling", "add a collection to sync", or anything `mb git-sync …`.
4
+ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
5
+ ---
6
+
7
+ # git-sync (representations ↔ instance)
8
+
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
+
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
+
13
+ ## Adding / removing a directory (collection) to sync
14
+
15
+ The set of directories under sync is governed by which **collections** carry `is_remote_synced: true`. Every collection so flagged serializes to its own folder under `collections/` in the repo; everything outside that set is local-only. The CLI exposes per-collection toggles that route to the underlying bulk endpoint (`PUT /api/ee/remote-sync/settings`):
16
+
17
+ ```bash
18
+ mb git-sync add-collection <collection-id> --profile <n> --json
19
+ mb git-sync remove-collection <collection-id> --profile <n> --json
20
+ ```
21
+
22
+ `<collection-id>` is a **positive integer**. The bulk endpoint's schema is `pos-int? → boolean`; nano-id / `root` / `trash` refs (which `collection get` accepts) are not supported here. Get the id from `mb collection list --profile <n> --json` first.
23
+
24
+ Both verbs return `{ success: true, task_id?: <id> }`. The optional `task_id` only appears when the toggle triggered a follow-up task (e.g., a finalization import after switching to read-only mode); for a normal add/remove in read-write, expect `{ success: true }` and nothing else.
25
+
26
+ **Cascade.** A toggle on a parent cascades to every descendant by `location` prefix — `add-collection 4` flips `4` plus every collection nested under it. `remove-collection 4` is the symmetric inverse. There is no per-leaf-only mode.
27
+
28
+ **Mode prerequisite.** The server rejects toggles while `remote-sync-type` is `:read-only` (the install default). If `mb git-sync add-collection 12` returns `Metabase returned 400 … Cannot change synced collections when remote-sync-type is read-only.`, switch first with:
29
+
30
+ ```bash
31
+ mb setting set remote-sync-type '"read-write"' --profile <n>
32
+ ```
33
+
34
+ (Mind the inner double quotes — `setting set` parses the value as strict JSON.) The server also rejects switching to `:read-only` while the Remote Sync collection is dirty; export or `--force` import first if you're going the other way.
35
+
36
+ **Verifying the result.** The CLI's `Collection` schema doesn't yet expose `is_remote_synced`, so `collection get --json` won't show the flag. The pragmatic confirmation paths are:
37
+
38
+ - `mb git-sync is-dirty --profile <n> --json` after editing a card in the now-synced collection — a `true` reading proves it's tracked.
39
+ - The Metabase Admin UI's Remote Sync page renders the per-collection toggles.
40
+
41
+ ## Read state before mutating
42
+
43
+ Always run `status` (or `is-dirty` + `has-remote-changes`) before `import` or `export`. Importing on a dirty instance silently rejects unless you pass `--force`; exporting when the instance is behind the remote pushes a stale state.
44
+
45
+ ```bash
46
+ mb git-sync status --profile <n> --json # → branch, dirty, current task
47
+ mb git-sync is-dirty --profile <n> --json # → {dirty: bool}; instance has unexported changes
48
+ mb git-sync has-remote-changes --profile <n> --json # → {behind: bool}; remote has unimported commits
49
+ mb git-sync dirty --profile <n> --json # → list the dirty objects
50
+ mb git-sync current-task --profile <n> --json # → in-flight task (or idle)
51
+ ```
52
+
53
+ **Clean up before exporting.** If you've created entities you intend to delete (a failed transform you're going to retry, a card you authored to test a body shape, a draft dashboard) — do the deletes _before_ the first `git-sync export`. Once committed, the cleanup needs a second commit, and the failed entity stays visible in `git log` forever. For the transform case specifically, prefer `transform update <id>` over `delete + create` so iteration never produces "broken-then-fixed" pairs in git history; see the `transform` skill, "Iterating on a failing transform".
54
+
55
+ ## Import (remote → instance)
56
+
57
+ ```bash
58
+ mb git-sync import --branch <branch> --profile <n>
59
+ # Default flags: --wait, polling --interval 2000 --timeout 600000
60
+ ```
61
+
62
+ Pulls the configured branch and applies it to the instance. Polls until the task reaches a terminal state (`succeeded` / `failed`).
63
+
64
+ | Flag | Purpose |
65
+ | ----------------- | ------------------------------------------------------------------------------------ |
66
+ | `--branch <name>` | Defaults to the `remote-sync-branch` setting; override per-call. |
67
+ | `--no-wait` | Return as soon as the task is queued; combine with `mb git-sync wait` later. |
68
+ | `--force` | **Discards local Metabase-side dirty changes** (lossy). Confirm with the user first. |
69
+ | `--timeout <ms>` | Polling deadline. Default 600 000. |
70
+ | `--interval <ms>` | Polling cadence. Default 2 000. |
71
+
72
+ Workflow:
73
+
74
+ 1. `git-sync status` — confirm `dirty: false` (or `--force` is intended).
75
+ 2. `git-sync has-remote-changes` — confirm there's actually something to import.
76
+ 3. `git-sync import --branch <branch>` — runs to terminal status by default.
77
+
78
+ ### First import on a fresh workspace
79
+
80
+ After `workspace start --repo …` brings up a brand-new workspace, the repo content **must be applied** before any other work — without it the instance has none of the repo content and subsequent edits will diverge from what's on disk.
81
+
82
+ The container runs a boot-time auto-import on first start, so in most cases the import has already completed by the time `workspace start --wait` returns. Check `git-sync status` first — if `current_task.sync_task_type == "import"` with `status == "successful"` and `.branch` matches the host's branch, you're done; skip the explicit call (it's a wasted round-trip). Only run the explicit `git-sync import` when the auto-import hasn't landed yet.
83
+
84
+ When you do need the explicit import, the first one on a fresh instance can report `status: conflict` (typically `conflicts: ["Transforms"]`) even when nothing is dirty — the boot-time auto-import sometimes leaves a stale task record that the first explicit import collides with. Retry the same command once; the second call usually succeeds. If it keeps reporting conflict, `git-sync import --force` is safe in this specific case because the workspace is empty — there's no instance-side work for `--force` to discard. (This is a narrow exception to the usual "confirm with the user before `--force`" rule.)
85
+
86
+ ```bash
87
+ HOST_BRANCH=$(git -C <repo-path> symbolic-ref --short HEAD)
88
+ SYNC_STATUS=$(mb git-sync status --profile <ws-name> --json)
89
+ if ! echo "$SYNC_STATUS" | jq -e --arg b "$HOST_BRANCH" \
90
+ '.current_task.sync_task_type == "import" and .current_task.status == "successful" and (.branch == $b)' >/dev/null; then
91
+ mb git-sync import --branch "$HOST_BRANCH" --profile <ws-name> --json \
92
+ || mb git-sync import --branch "$HOST_BRANCH" --profile <ws-name> --json \
93
+ || mb git-sync import --branch "$HOST_BRANCH" --force --profile <ws-name> --json
94
+ fi
95
+ ```
96
+
97
+ ## Export (instance → remote)
98
+
99
+ ```bash
100
+ mb git-sync export -m "commit message" --branch <branch> --profile <n>
101
+ ```
102
+
103
+ Pushes Metabase-side changes back to the configured remote. `-m` is the commit message; without it the server picks a default. Defaults to `--wait`.
104
+
105
+ | Flag | Purpose |
106
+ | ------------------- | -------------------------------------------------------- |
107
+ | `--branch <name>` | Push to a specific branch instead of the configured one. |
108
+ | `-m, --message <s>` | Commit message. |
109
+ | `--force` | Force-push / overwrite remote. Confirm with the user. |
110
+ | `--no-wait` | Don't poll. |
111
+
112
+ Workflow:
113
+
114
+ 1. **Branch guard** (below) — confirm the workspace isn't tracking `main`/`master`, or that the user has explicitly accepted exporting to it.
115
+ 2. `git-sync is-dirty` — confirm there's something to export.
116
+ 3. `git-sync export -m "..."` — pushes and polls.
117
+ 4. (Optional) `git-sync status` — verify `dirty: false` after.
118
+ 5. **Working-tree drift** (below) — if this is a `--repo` bind-mount workspace, the host repo's working tree + index will lag behind the new HEAD. Surface this and offer to realign.
119
+
120
+ ### Branch guard: don't export to main/master without confirmation
121
+
122
+ Workspace work is conventionally done on a feature branch — exporting to `main` (or `master`) commits team-shared content directly. Before `git-sync export`, check the tracked branch and if it's `main`/`master`, ask the user whether to switch first.
123
+
124
+ Reading the current branch:
125
+
126
+ - For a `--repo` bind-mount workspace, `git -C <repo-path> symbolic-ref --short HEAD` is the most reliable read — that's what the workspace's `remote-sync-branch` was bound to at start time.
127
+ - Otherwise: `mb git-sync status --profile <n> --json | jq -r '.branch'`.
128
+
129
+ If the branch is `main` or `master`, prompt with `AskUserQuestion`:
130
+
131
+ > "The workspace is tracking `<branch>` — exporting commits straight to it. Switch to a feature branch first?"
132
+ >
133
+ > 1. **Create a feature branch via the workspace** — agent suggests a name (e.g., `agent/<task>`); run `mb git-sync create-branch <name> --profile <n>`. This exports current dirty state to the new branch and switches the workspace's tracked branch to it; subsequent `git-sync export` calls go to that branch.
134
+ > 2. **Switch the host's branch first (bind-mount workspaces)** — `git -C <repo> checkout -b <name>` on the host, then pass `--branch <name>` on the next `git-sync export` so the export targets the new branch (the workspace's `remote-sync-branch` setting won't auto-update from a host-side checkout).
135
+ > 3. **Proceed on `main`/`master`** — explicitly accepted; surface the resulting commit (`git -C <repo> log --oneline -1`) afterwards so the user can amend or revert.
136
+
137
+ Skip the prompt only if the user's instructions already specified the branch (e.g., they explicitly said "export to main" or named a feature branch). Don't silently default to whatever `remote-sync-branch` happens to point at.
138
+
139
+ ### Post-export: working-tree drift on `--repo` bind-mount workspaces
140
+
141
+ When the workspace exports against a host bind mount, the in-container serializer writes the new commit object directly into the bind-mounted `.git/` (creating tree/blob objects and advancing the branch ref) but **does not update the host's working tree or index**. After a successful export, the host repo state is:
142
+
143
+ - HEAD: the new export commit.
144
+ - Index: still matches the _previous_ HEAD (whatever the user had staged before).
145
+ - Working tree: still matches the _previous_ HEAD.
146
+
147
+ `git status` then shows "Changes to be committed" that look like the export's content reverting back — purely a display artifact, not an actual revert. The container does this on purpose to avoid clobbering work-in-progress on the host. **Realigning is _applying_ the new HEAD's content to your worktree, not discarding work** — the new commit was written by the exporter, not by your local edits, and your tree/index are stale relative to the new HEAD until you realign.
148
+
149
+ **Surface this to the user** after an export against a `--repo` workspace — don't leave them staring at a confusing `git status`. Offer to realign.
150
+
151
+ **Prefer `git restore` over `git reset --hard`.** When the only "changes" are the drift artifact (no real local edits), `git restore` does the same job and isn't classified as a destructive operation by Claude Code's permission system — `git reset --hard` is, and gets blocked even after a user-confirmation dialog:
152
+
153
+ ```bash
154
+ git -C <repo> restore --staged --worktree . # non-destructive; aligns index + working tree to HEAD
155
+ ```
156
+
157
+ This is the right default after a `git-sync export` realignment when the user had nothing else staged. If `git status` shows a mix of drift artifacts and real pending work, fall back to the stash sequence:
158
+
159
+ ```bash
160
+ git -C <repo> stash --include-untracked
161
+ git -C <repo> restore --staged --worktree .
162
+ git -C <repo> stash pop
163
+ ```
164
+
165
+ `git reset --hard HEAD` is the canonical equivalent and still valid — but **confirm with the user** before running it, and expect Claude Code to gate it as destructive even after the dialog. `git restore --staged --worktree .` produces the same end-state with less friction.
166
+
167
+ Or pull in the new files selectively with `git -C <repo> checkout HEAD -- <path>`. Quick check that this is what you're seeing: `git -C <repo> diff --cached HEAD~1 --stat` returns empty (the index matches the parent commit, not the new HEAD).
168
+
169
+ ## Branches
170
+
171
+ ```bash
172
+ mb git-sync branches --profile <n> --json # list remote branches
173
+ mb git-sync create-branch <name> --profile <n> # create + switch sync to it
174
+ mb git-sync stash --profile <n> # export current state to a NEW branch
175
+ ```
176
+
177
+ `stash` is the safe move when the instance has team work you don't want to lose, but you need to pivot to a different branch (`import` would discard, `export --force` would overwrite). It exports current state to a fresh branch first.
178
+
179
+ ## Polling and cancelling
180
+
181
+ ```bash
182
+ mb git-sync wait --profile <n> # block on the in-flight task
183
+ mb git-sync cancel-task --profile <n> # cancel the in-flight task
184
+ ```
185
+
186
+ Use `wait` after `import --no-wait` / `export --no-wait`. Use `cancel-task` if a git-sync task hangs and you want to abandon it.
187
+
188
+ ## Don't (git-sync-specific)
189
+
190
+ - Don't run `git-sync import --force` or `git-sync export --force` without explicit user confirmation. Both are lossy — `--force` import discards instance-side work, `--force` export overwrites the remote branch.
191
+ - Don't drive `git-sync` against a Metabase instance that doesn't have remote-sync configured — every verb returns an error pointing at the missing `remote-sync-*` settings. To check: `mb setting get remote-sync-url --profile <n> --json`.
192
+ - Don't author content directly via `card create` / `transform create` and then assume `git-sync export` will commit it cleanly — the instance and repo can drift if you mix direct API writes with sync-tracked changes. If you do, follow direct writes immediately with `git-sync export -m "..."` to keep them in step.
193
+ - Don't omit `-m` on `export` if the user wants a meaningful commit message — the default server-generated message is generic.
194
+ - Don't `git-sync export` to `main`/`master` without explicit user confirmation — workspace work is conventionally on a feature branch. See "Branch guard" above.
195
+ - Don't pretend the host's `git status` is clean after `git-sync export` against a `--repo` bind mount — the export advances HEAD but leaves the working tree + index behind. See "Working-tree drift" above.
196
+ - Don't reach for `mb setting set` to mark a collection as remote-synced — that endpoint writes single-key settings, not the bulk `collections` map. Use `mb git-sync add-collection <id>` / `mb git-sync remove-collection <id>` (see "Adding / removing a directory (collection) to sync" above), and remember the toggle cascades to descendants.
@@ -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
+ ```