@metabase/cli 0.1.2 → 0.1.4-alpha.skill-packaging.1c8ec40

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 (189) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/README.md +467 -388
  3. package/dist/add-collection-B3NhkLRT.mjs +11 -0
  4. package/dist/{add-collection-DwxQDXzL.mjs → add-collection-CEvZTea_.mjs} +5 -5
  5. package/dist/{api-key-BktzvPb7.mjs → api-key-BpmnzLLb.mjs} +1 -1
  6. package/dist/{archive-CLWtbvvH.mjs → archive-BPLLOdfL.mjs} +7 -7
  7. package/dist/{archive-C1mF-9Kj.mjs → archive-CazAWIrV.mjs} +6 -6
  8. package/dist/{archive-kYoy5LK5.mjs → archive-D596Cq7R.mjs} +7 -7
  9. package/dist/{archive-Cq4WKmJt.mjs → archive-DqPqXMZm.mjs} +7 -7
  10. package/dist/auth-DJp4QoWI.mjs +19 -0
  11. package/dist/{body-rDrR-C1c.mjs → body-CVSMGqZk.mjs} +3 -3
  12. package/dist/{branches-CH2UcCpX.mjs → branches--TavvEK1.mjs} +7 -7
  13. package/dist/{cancel-CgLZcItQ.mjs → cancel-BUitag4_.mjs} +6 -6
  14. package/dist/{cancel-task-DcYrFsM6.mjs → cancel-task-BHcWJUMu.mjs} +7 -7
  15. package/dist/card-DtUIadDM.mjs +20 -0
  16. package/dist/{cards-C4NIaERo.mjs → cards-Bir6wRS_.mjs} +6 -6
  17. package/dist/cli.mjs +31 -27
  18. package/dist/collection-4-gZeCWg.mjs +19 -0
  19. package/dist/{create-CNvd5T8h.mjs → create-4OjxTGAe.mjs} +10 -10
  20. package/dist/{create-bqc_rmix.mjs → create-BISpvrml.mjs} +10 -10
  21. package/dist/{create-BUCLNqiN.mjs → create-Bi8sqzL-.mjs} +13 -13
  22. package/dist/{create-Cbh1cGj9.mjs → create-C1cdoSw5.mjs} +12 -12
  23. package/dist/{create-Dh0p-c2Y.mjs → create-C3TiMJhQ.mjs} +10 -10
  24. package/dist/{create-QgN369N5.mjs → create-CZZc1KM8.mjs} +13 -13
  25. package/dist/{create-DU0ZhnZu.mjs → create-CxMpPsRO.mjs} +9 -9
  26. package/dist/{create-CB0Yp__0.mjs → create-Dj-fO-nV.mjs} +12 -12
  27. package/dist/{create-DvrVZ2hS.mjs → create-DuyWlx-S.mjs} +11 -11
  28. package/dist/{create-branch-BJFH9Hda.mjs → create-branch-CH1FmnxS.mjs} +7 -7
  29. package/dist/{create-CzfNOhOF.mjs → create-hYdLOyw_.mjs} +12 -12
  30. package/dist/{credentials-DTP1xuKz.mjs → credentials-BARfQeBr.mjs} +11 -9
  31. package/dist/{current-task-z_TiJ0kt.mjs → current-task-TX7rJmon.mjs} +7 -7
  32. package/dist/dashboard-C-azCGk-.mjs +20 -0
  33. package/dist/{database-DQkUxTLd.mjs → database-BKfGtTmB.mjs} +3 -3
  34. package/dist/db-D--IArxI.mjs +22 -0
  35. package/dist/{delete-DeZQ1r9w.mjs → delete-BxUaF8kW.mjs} +8 -8
  36. package/dist/{delete-CVYII8mq.mjs → delete-DzTd5w_y.mjs} +8 -8
  37. package/dist/{delete-runtime-BMzvfj_B.mjs → delete-runtime-DfFMWJJ6.mjs} +2 -2
  38. package/dist/{delete-table-ZiR9-ndv.mjs → delete-table-DWfA7uVn.mjs} +8 -8
  39. package/dist/{deprovision-BhD3J-Am.mjs → deprovision-CadXjs_7.mjs} +12 -12
  40. package/dist/{dirty-D9agt7Os.mjs → dirty-an-DQtBG.mjs} +7 -7
  41. package/dist/{docker-CHpV8PRz.mjs → docker-C9WQCjkt.mjs} +6 -103
  42. package/dist/{eid-B5wawMmO.mjs → eid-Cz9r5RX5.mjs} +1 -1
  43. package/dist/{export-Bfk7JAlR.mjs → export-BHq8ztFL.mjs} +10 -10
  44. package/dist/field-Do1HcUfq.mjs +18 -0
  45. package/dist/{fields-7ByLsxLg.mjs → fields-B21DeL2I.mjs} +6 -6
  46. package/dist/{flag-pair-DtR1AiBQ.mjs → flag-pair-Fmcdkrfx.mjs} +1 -1
  47. package/dist/{get-DikegGzi.mjs → get-BAqeeIQ-.mjs} +6 -6
  48. package/dist/{get-tISo-cmg.mjs → get-BHCgwPPT.mjs} +9 -9
  49. package/dist/{get-CQGeF-eP.mjs → get-BPah4iYM.mjs} +6 -6
  50. package/dist/{get-DKy3DAJX.mjs → get-C4_7ZrIS.mjs} +7 -7
  51. package/dist/{get-cuHp9-6U.mjs → get-CAYnc9sd.mjs} +6 -6
  52. package/dist/{get-BE6Izpus.mjs → get-ChEFd1sT.mjs} +7 -7
  53. package/dist/{get-gOT_RarI.mjs → get-ChxcxY2D.mjs} +7 -7
  54. package/dist/{get-DUSR5i99.mjs → get-Cy9YIesM.mjs} +6 -6
  55. package/dist/{get-bYc7eGYe.mjs → get-D-zU7VmK.mjs} +6 -6
  56. package/dist/{get-D2m4jhwT.mjs → get-DFm7pR7F.mjs} +8 -8
  57. package/dist/get-DWttywn_.mjs +79 -0
  58. package/dist/{get-StkjKuh0.mjs → get-RPL4h0It.mjs} +8 -8
  59. package/dist/{get-C3CcAJGg.mjs → get-dqrzLuwA.mjs} +8 -8
  60. package/dist/{get-run-D59Yqaoh.mjs → get-run-CG3QFs9T.mjs} +7 -7
  61. package/dist/git-sync-COKsdLkS.mjs +28 -0
  62. package/dist/{has-remote-changes-B1TciDVD.mjs → has-remote-changes-ecra9BFO.mjs} +7 -7
  63. package/dist/{import-DnnmmJbp.mjs → import-BzOKkYnp.mjs} +10 -10
  64. package/dist/{input-ikCiip6x.mjs → input-BQ-BZA8h.mjs} +1 -1
  65. package/dist/{is-dirty-DlfX7e39.mjs → is-dirty-7afLBOtn.mjs} +4 -4
  66. package/dist/is-dirty-Dnw0AHFw.mjs +10 -0
  67. package/dist/{items-DQFQSpjF.mjs → items-BEWSuEfe.mjs} +10 -10
  68. package/dist/{key-NDEARu2L.mjs → key-CCJdVWKc.mjs} +1 -1
  69. package/dist/{license-DBh13sc8.mjs → license-D3mFdxAq.mjs} +3 -3
  70. package/dist/{list-DQj-QJAs.mjs → list-B5E7LFDb.mjs} +8 -8
  71. package/dist/{list-BwjqQ6pp.mjs → list-B9mEqMZ5.mjs} +5 -5
  72. package/dist/{list-D067ZSE5.mjs → list-BJAC356Q.mjs} +7 -7
  73. package/dist/{list-Di529OJD.mjs → list-BOQWvb8b.mjs} +5 -5
  74. package/dist/{list-Cy0VhXQs.mjs → list-BZFTuiBr.mjs} +6 -6
  75. package/dist/{list-DJN-OvTZ.mjs → list-By3t67Y8.mjs} +7 -7
  76. package/dist/{list-GFfR9SuT.mjs → list-ByPzCM2G.mjs} +5 -5
  77. package/dist/{list-9AOWhxqp.mjs → list-C8b-KxY6.mjs} +8 -8
  78. package/dist/{list-iFVEdi2J.mjs → list-CiHlWufc.mjs} +6 -6
  79. package/dist/{list-DlKzgnqo.mjs → list-D_exD3LP.mjs} +7 -7
  80. package/dist/{list-4kYCGv01.mjs → list-DiAyv9l3.mjs} +6 -6
  81. package/dist/{list-CP5RNjO6.mjs → list-DtSOfMZ2.mjs} +6 -6
  82. package/dist/list-DwF6on6O.mjs +44 -0
  83. package/dist/{list-DAZP-IM5.mjs → list-nIxzXqr-.mjs} +5 -5
  84. package/dist/{login-DxgkosGx.mjs → login-Cka2HZWz.mjs} +9 -9
  85. package/dist/{logout-BlVwqBog.mjs → logout-rA25M-oa.mjs} +6 -6
  86. package/dist/{logs-CudNEkT4.mjs → logs-B5cGXoDi.mjs} +11 -10
  87. package/dist/{manifest-Dv5B9Blc.mjs → manifest-CGM7XNLC.mjs} +2 -2
  88. package/dist/measure-CwJ2o89T.mjs +19 -0
  89. package/dist/{metadata-BTJAFVvZ.mjs → metadata-BEd6s_Qf.mjs} +6 -6
  90. package/dist/{metadata-B2Td415K.mjs → metadata-DirWO0cS.mjs} +6 -6
  91. package/dist/{package-DV6Asqim.mjs → package-VV3qWWIQ.mjs} +10 -3
  92. package/dist/{parse-id-B38zTlYs.mjs → parse-id-DbSjfzoU.mjs} +1 -1
  93. package/dist/{parse-ref-DGvh4aDn.mjs → parse-ref-D1yeDOn8.mjs} +1 -1
  94. package/dist/{parse-schemas-Ds-cVE-O.mjs → parse-schemas-BXTlpoaf.mjs} +2 -2
  95. package/dist/path-DarsuSkB.mjs +58 -0
  96. package/dist/{poll-Bh6oAifO.mjs → poll-Cd8bRMmZ.mjs} +2 -2
  97. package/dist/{poll-task-vPwV31Fs.mjs → poll-task-BwKuhZ9P.mjs} +2 -2
  98. package/dist/{predicates-DiIiS3k7.mjs → predicates-CGO17Q15.mjs} +1 -1
  99. package/dist/{preflight-DxJb-hUV.mjs → preflight-RkDLqtNm.mjs} +3 -3
  100. package/dist/process-zJeVJZTM.mjs +105 -0
  101. package/dist/{prompt-Bf3DQ-qE.mjs → prompt-DgDNy_Pc.mjs} +1 -1
  102. package/dist/{provision-B-I0zuDe.mjs → provision-DBqPA-De.mjs} +15 -15
  103. package/dist/{ps-CaiOFCv2.mjs → ps-Cn3lzak-.mjs} +4 -4
  104. package/dist/ps-DIm7IScc.mjs +11 -0
  105. package/dist/{query-BtF1yWZZ.mjs → query-BoOismbH.mjs} +13 -13
  106. package/dist/{query-jmfqaXRP.mjs → query-CkNa5Nam.mjs} +9 -9
  107. package/dist/{remove-xskleeru.mjs → remove-B7NUBOqF.mjs} +6 -6
  108. package/dist/{remove-C2iv0g03.mjs → remove-C08c3vne.mjs} +10 -9
  109. package/dist/{remove-collection-DhZghaZy.mjs → remove-collection-cFO9M-qV.mjs} +9 -9
  110. package/dist/{render-DXv-D6fU.mjs → render-DuoDUTVL.mjs} +1 -1
  111. package/dist/{rescan-values-DW6u90ep.mjs → rescan-values-D1-IyyU7.mjs} +6 -6
  112. package/dist/{revision-message-flag-CWQbKhdl.mjs → revision-message-flag-oyq2xrDU.mjs} +1 -1
  113. package/dist/{run-DxVzhcF3.mjs → run-BCzKf_TV.mjs} +9 -9
  114. package/dist/{runs-BOHk1XnM.mjs → runs-pVgTv7cK.mjs} +9 -9
  115. package/dist/{runtime-cwBS8wwK.mjs → runtime-DPdP0sl1.mjs} +6 -6
  116. package/dist/{schema-tables-CcFbY_jN.mjs → schema-tables-c6K9AkS-.mjs} +6 -6
  117. package/dist/{schemas-DZmv_V62.mjs → schemas-s3PRMTwc.mjs} +6 -6
  118. package/dist/{search-CYMuc7Fg.mjs → search-DsF08cm9.mjs} +8 -8
  119. package/dist/segment-CvPF5gLG.mjs +19 -0
  120. package/dist/{set-CbGfQ7Ye.mjs → set-BNQjt8S1.mjs} +12 -12
  121. package/dist/{set-B_rrVwU4.mjs → set-DlfevPxG.mjs} +9 -9
  122. package/dist/{setting-DqZY9NXP.mjs → setting-Cc5FWSNN.mjs} +3 -3
  123. package/dist/{setup-DxmcAorA.mjs → setup-CIsvZgEA.mjs} +9 -9
  124. package/dist/skills-CHU7uuDU.mjs +191 -0
  125. package/dist/skills-Dws-azxD.mjs +18 -0
  126. package/dist/snippet-W-TaXdio.mjs +19 -0
  127. package/dist/{start-Cn0epTks.mjs → start-xREv6hev.mjs} +19 -17
  128. package/dist/{stash-BFZIl9F4.mjs → stash-ZLbOejYk.mjs} +9 -9
  129. package/dist/{status-UALK3OJl.mjs → status-CYWbB67d.mjs} +5 -5
  130. package/dist/{status-FDIDmqvM.mjs → status-DiXaGNmb.mjs} +5 -5
  131. package/dist/{status-BjCeJNLp.mjs → status-DjRAgzXl.mjs} +8 -8
  132. package/dist/{stop-DUwrDWw8.mjs → stop-CWKn_jI8.mjs} +10 -9
  133. package/dist/{summary-CS4UGiFJ.mjs → summary-CIFwiame.mjs} +6 -6
  134. package/dist/{sync-schema-IrHdJxmX.mjs → sync-schema-_SRMfBSQ.mjs} +6 -6
  135. package/dist/table-uhBlfNYj.mjs +19 -0
  136. package/dist/transform-UJ7T_BSb.mjs +24 -0
  137. package/dist/transform-job-DXs5apQ1.mjs +19 -0
  138. package/dist/{translate-B__zbDKm.mjs → translate-DVN_sIVb.mjs} +10 -10
  139. package/dist/{tree-Mh0uQ_Wy.mjs → tree-BTZfgldu.mjs} +5 -5
  140. package/dist/{update-D2VI_5cy.mjs → update-Bjd7YRyh.mjs} +13 -13
  141. package/dist/{update-Bw0WZix_.mjs → update-CCFegz4q.mjs} +15 -15
  142. package/dist/{update-BfBsM_y1.mjs → update-CDdML0mE.mjs} +14 -14
  143. package/dist/{update-B9DBMo30.mjs → update-CGYM_wWQ.mjs} +12 -12
  144. package/dist/{update-Cp1789qq.mjs → update-CYMsVUQX.mjs} +10 -10
  145. package/dist/{update-j9vgemKR.mjs → update-D48EYMKn.mjs} +10 -10
  146. package/dist/{update-1Di9hbPo.mjs → update-D6jptFDw.mjs} +14 -14
  147. package/dist/{update-B5_pp6Jj.mjs → update-DSjj9UQb.mjs} +15 -15
  148. package/dist/{update-Masp5WeT.mjs → update-DjfNbEgo.mjs} +11 -11
  149. package/dist/{update-D8GwQTcL.mjs → update-ZRF-sYqN.mjs} +16 -16
  150. package/dist/{update-dashcard-CNiQw1MD.mjs → update-dashcard-BfcB6pFx.mjs} +10 -10
  151. package/dist/upgrade-Cw_mP31_.mjs +432 -0
  152. package/dist/{url-GFM76VIK.mjs → url-DsDSVEnK.mjs} +9 -8
  153. package/dist/{uuid-Uif0lNk8.mjs → uuid-DbtHyH33.mjs} +7 -7
  154. package/dist/{validate-DCYx6jdL.mjs → validate-CB0bu50i.mjs} +3 -3
  155. package/dist/{validate-query-B07oGG4K.mjs → validate-query-CavIA0Q2.mjs} +3 -3
  156. package/dist/{values-DrwNHUAI.mjs → values-C1JRC3O2.mjs} +6 -6
  157. package/dist/{wait-DO7tS7NI.mjs → wait-Ed4Q7ccW.mjs} +2 -2
  158. package/dist/{wait-BoKk8CJy.mjs → wait-LRmiGepb.mjs} +8 -8
  159. package/dist/{wait-flags-CjX2sEGm.mjs → wait-flags-pItBfuzi.mjs} +2 -2
  160. package/dist/workspace-BozpZKez.mjs +24 -0
  161. package/dist/{workspace-credentials-B6BL-X0d.mjs → workspace-credentials-4lIxxz4g.mjs} +2 -41
  162. package/dist/yaml-ECiog374.mjs +43 -0
  163. package/package.json +8 -3
  164. package/skill-data/core/SKILL.md +575 -0
  165. package/skill-data/git-sync/SKILL.md +196 -0
  166. package/skill-data/transform/SKILL.md +235 -0
  167. package/skill-data/workspace/SKILL.md +408 -0
  168. package/skills/metabase-cli/SKILL.md +42 -0
  169. package/dist/add-collection-SL08iMub.mjs +0 -11
  170. package/dist/auth-DfYkakP3.mjs +0 -19
  171. package/dist/card-ZCGU2JEh.mjs +0 -20
  172. package/dist/collection-D_uFLIAS.mjs +0 -19
  173. package/dist/dashboard-G1-dGLUR.mjs +0 -20
  174. package/dist/db-CBaEfumR.mjs +0 -22
  175. package/dist/field-BDJ1pEgr.mjs +0 -18
  176. package/dist/git-sync-BiTWfLgY.mjs +0 -28
  177. package/dist/is-dirty-DClGFOGV.mjs +0 -10
  178. package/dist/measure-C7SbdYQk.mjs +0 -19
  179. package/dist/ps-BmYQYC7t.mjs +0 -10
  180. package/dist/segment-Df4pfjco.mjs +0 -19
  181. package/dist/snippet-CwSHjQyn.mjs +0 -19
  182. package/dist/table-Cdr5bKp1.mjs +0 -19
  183. package/dist/transform-CeZusR_w.mjs +0 -24
  184. package/dist/transform-job-BOn9-CGa.mjs +0 -19
  185. package/dist/workspace-CyEX40D-.mjs +0 -24
  186. /package/dist/{snippet-Dw0Sjzkr.mjs → snippet-CSWqkslB.mjs} +0 -0
  187. /package/dist/{transform-IEX4Mx3X.mjs → transform-DR4ejuPM.mjs} +0 -0
  188. /package/dist/{transform-job-Csr86muI.mjs → transform-job-BrhOLO4M.mjs} +0 -0
  189. /package/dist/{workspace-DVuqKJGG.mjs → workspace-DUfqhPm5.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, also load the `metabase-representation-format` skill — it covers the file-tree layout and per-resource YAML shape.
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,235 @@
1
+ ---
2
+ name: transform
3
+ description: Author and run Metabase transforms via `mb` — body shape (native SQL + MBQL 5), create + run-with-wait, run inspection, cancel, the `update`-vs-recreate iteration rule, and the writable-keys-only PATCH contract. Load when the user touches transforms — "create a transform", "run a transform", "fix a failing transform", "list transform runs", "cancel a running transform", or anything `mb transform …`.
4
+ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
5
+ ---
6
+
7
+ # Transforms
8
+
9
+ A **transform** persists the result of a query (native SQL or MBQL) to a warehouse table the user can read from cards, dashboards, and other transforms. It runs on a schedule (via `transform-job`) or on-demand (`transform run`).
10
+
11
+ This skill covers the create-and-run flow. The general flag conventions, body-input precedence, and output flags live in the `core` skill (`mb skills get core`). If you're authoring a transform inside a workspace, also load the `workspace` skill for the canonical-vs-isolation-schema rule.
12
+
13
+ ## Body shape
14
+
15
+ A transform has two halves:
16
+
17
+ - `source` — the query to run (`type: "query"`, with `query.type` of `native` or `mbql`).
18
+ - `target` — the warehouse destination (`type: "table"`, with `database`, `schema`, `name`).
19
+
20
+ Native SQL is the simplest source and the easiest to author by hand. 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.
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 authority — useful 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`.
61
+
62
+ ## Create + run (native SQL)
63
+
64
+ ```bash
65
+ cat > /tmp/transform.json <<'EOF'
66
+ {
67
+ "name": "user_counts_by_signup_year",
68
+ "description": "Sample transform: counts users by year of signup",
69
+ "source": {
70
+ "type": "query",
71
+ "query": {
72
+ "type": "native",
73
+ "database": <db-id>,
74
+ "native": {
75
+ "query": "SELECT date_trunc('year', created_at)::date AS signup_year, COUNT(*)::int AS user_count FROM public.users GROUP BY 1 ORDER BY 1"
76
+ }
77
+ }
78
+ },
79
+ "target": {
80
+ "type": "table",
81
+ "database": <db-id>,
82
+ "schema": "public",
83
+ "name": "user_counts_by_signup_year"
84
+ }
85
+ }
86
+ EOF
87
+
88
+ TRANSFORM_ID=$(mb transform create --file /tmp/transform.json --profile <name> --json | jq -r '.id')
89
+ mb transform run "$TRANSFORM_ID" --wait --profile <name> --json
90
+ ```
91
+
92
+ Notes:
93
+
94
+ - `<db-id>` comes from `mb database list --profile <name> --json`. Database ids are per-instance — a workspace child re-numbers them independently of the parent.
95
+ - Target `schema` is the **canonical** name (e.g. `public`). In a workspace, the QP rewrites it to the per-workspace isolation schema (`mb__isolation_<hash>_<ws-id>`) at execution time — don't hard-code that prefix.
96
+ - `--wait` on `transform run` polls until status is `succeeded` or `failed`. Without it you only get `{message: "Transform run started", run_id, final: null}` and have to poll yourself.
97
+ - The `--json` envelope is shape-stable: `{message, run_id, final}`. `final` is always present — `null` when `--wait` is omitted or the run never started, otherwise a full `TransformRun` object with `status` and `message`. On a failed run (`final.status` ∈ {`failed`, `timeout`, `canceled`}) the CLI exits 1 and writes a one-line summary `transform run <id> failed` to stderr; the failure detail lives only in `final.message` on stdout, so `jq -r '.final.message'` is where to look.
98
+ - The heredoc with single-quoted `'EOF'` prevents shell from interpolating any `$vars` inside the SQL.
99
+ - `transform create --json` returns the agent-facing compact projection: `{id, name, description, source_type, target: {type, database, schema, name}, target_db_id}`. Read `target.schema`/`target.name` directly off the create output — no follow-up `transform get` needed to verify where the transform will write.
100
+ - If a transform with the same `name` already has a YAML representation on disk under the configured remote-sync repo, `create` mints a `_2` suffix on the exported filename (the new transform gets a fresh `entity_id`; the prior one isn't touched). For "iterate on the same concept" workflows, prefer `transform update <id>` — see "Iterating on a failing transform" below.
101
+
102
+ ## Inspect
103
+
104
+ ```bash
105
+ mb transform list --profile <name> --json
106
+ mb transform get <id> --profile <name> --full --json # full transform incl. last run summary
107
+ ```
108
+
109
+ After a run, the materialized table is queryable via `mb` (`card create` against it, native query against `<schema>.<name>`, etc.). Columns and types are inferred from the result set; if you change the SELECT shape, drop the table first or the next run will fail on a column-mismatch error.
110
+
111
+ ## Inspect runs and cancel an in-flight run
112
+
113
+ ```bash
114
+ # Recent runs across all transforms (drains all pages by default; cap with --limit):
115
+ mb transform runs --profile <name> --json
116
+ mb transform runs --transform-id <id> --limit 10 --profile <name> --json
117
+
118
+ # Fetch one run by RUN id (NOT transform id — the run id comes from `transform run` or `transform runs`):
119
+ mb transform get-run <run-id> --profile <name> --json
120
+
121
+ # Cancel the currently-running run for a transform:
122
+ mb transform cancel <id> --profile <name> --json
123
+ ```
124
+
125
+ Notes:
126
+
127
+ - `transform runs` and `transform get-run` parse against the same `TransformRun` schema, so `get-run` returns the same per-run shape as one entry of `runs`. The compact projection is `{id, transform_id, status, run_method, start_time, end_time, message}`. Pass `--full` on `get-run` for the hydrated row including `is_active`, `user_id`, `transform_name`, `transform_entity_id`, `checkpoint_*` fields, and a nested `transform: {id, name, …}` block.
128
+ - `transform cancel` takes the **transform** id and 404s with `Endpoint not found — is this a Metabase instance?` if there is no active run. The response shape is `{canceled: true, id: <transform-id>}`.
129
+ - For native-SQL transforms, cancel marks the run as `canceling` but does **not** kill the warehouse query mid-flight — the query runs to completion, then the run lands as `canceled` (or stays `succeeded` if the cancel arrived after the writer committed). For Python transforms the worker is interrupted directly. Don't expect cancel to free warehouse resources instantly on long native queries; expect it to flip state and prevent downstream consumers from treating the result as good.
130
+ - The `--transform-id` filter on `runs` accepts a single integer; the CLI translates to the server's `transform-ids` query vector. To cross-filter multiple transforms, run `transform runs --json` and `jq` post-hoc.
131
+
132
+ ## Update body: send only writable keys, never round-trip the GET body
133
+
134
+ `transform update <id>` is **PATCH semantics** — only send the fields you actually want to change. The endpoint accepts exactly these writable keys:
135
+
136
+ ```
137
+ name, description, source, target, run_trigger,
138
+ tag_ids, collection_id, owner_user_id, owner_email
139
+ ```
140
+
141
+ **Don't paste the output of `transform get` into a `transform update` body.** The GET response carries server-side fields (`id`, `entity_id`, `created_at`, `updated_at`, `creator_id`, `last_run`, `target_db_id`, `target_table_id`, `source_type`, `source_database_id`, `source_readable`, `creator`, `owner`, `table`, …) that the PUT endpoint isn't built to handle. Currently, unknown top-level keys flow into `t2/update!` and produce a leaked H2 SQL error like:
142
+
143
+ ```
144
+ Column "TAGS" not found; SQL statement:
145
+ UPDATE "TRANSFORM" SET "TAGS" = (), "UPDATED_AT" = NOW() WHERE "ID" = ? [42122-214]
146
+ ```
147
+
148
+ Two specific footguns:
149
+
150
+ - **`tags` is not a key on the REST API.** The serdes/YAML representation uses `tags`; the REST contract uses `tag_ids` (an array of integer ids). If you pulled a YAML representation and want to PUT it, translate `tags: [...]` → `tag_ids: [...]` first (or omit it entirely if you're not changing tag membership).
151
+ - **`source_type`, `target_db_id`, `target_table_id`, `entity_id`** are derived/computed by the server. They appear in GET responses for the agent's benefit; the server doesn't accept them on update.
152
+
153
+ Right shape — patch only what changes:
154
+
155
+ ```bash
156
+ # Rename only:
157
+ mb transform update <id> --body '{"name":"renamed"}' --profile <name> --json
158
+
159
+ # Rewrite the SQL only:
160
+ cat > /tmp/patch.json <<'EOF'
161
+ { "source": { "type": "query", "query": { "type": "native",
162
+ "database": <db-id>,
163
+ "native": { "query": "SELECT … FROM public.orders" } } } }
164
+ EOF
165
+ mb transform update <id> --file /tmp/patch.json --profile <name> --json
166
+
167
+ # Change tag membership (note: tag_ids, not tags):
168
+ mb transform update <id> --body '{"tag_ids":[1,3]}' --profile <name> --json
169
+ ```
170
+
171
+ If you really must round-trip, project to the writable subset:
172
+
173
+ ```bash
174
+ mb transform get <id> --full --profile <name> --json \
175
+ | jq '{name, description, source, target, run_trigger, tag_ids, collection_id, owner_user_id, owner_email}
176
+ | with_entries(select(.value != null))' \
177
+ > /tmp/patch.json
178
+ ```
179
+
180
+ ## Iterating on a failing transform
181
+
182
+ When `transform run` fails and you want to retry with a fixed body, **prefer `transform update <id> --file body.json` over `transform delete <id>` + `transform create`.** Update keeps the same row, the same `entity_id`, the same materialized table, and the same on-disk YAML filename. Concretely this means:
183
+
184
+ - `git-sync export` produces **one** clean commit containing only the fix, instead of "broken transform" + "remove broken transform" landing as two commits in `git log`.
185
+ - You don't have to chase `_2` suffixes minted when two YAMLs share a `name` on disk (see the `transform create` notes above).
186
+ - The materialized output table either updates in place or, if the SELECT shape changed incompatibly, errors loudly on the next run rather than landing in a parallel `..._2` table the agent has to clean up. (`transform delete-table <id>` resets the column shape if you need a clean slate.)
187
+
188
+ Recipe:
189
+
190
+ ```bash
191
+ # 1. Try once
192
+ ID=$(mb transform create --file /tmp/t.json --profile <n> --json | jq -r '.id')
193
+ mb transform run "$ID" --wait --profile <n> --json # → failed
194
+
195
+ # 2. Fix the body in place; PATCH only what changed.
196
+ # Source-only patch — keeps name, target, tags untouched on the server.
197
+ cat > /tmp/source-patch.json <<'EOF'
198
+ { "source": { "type": "query", "query": { "type": "native",
199
+ "database": <db-id>,
200
+ "native": { "query": "<fixed SQL here>" } } } }
201
+ EOF
202
+ mb transform update "$ID" --file /tmp/source-patch.json --profile <n> --json
203
+
204
+ # 3. Re-run
205
+ mb transform run "$ID" --wait --profile <n> --json # → succeeded
206
+ ```
207
+
208
+ If you really must `create + delete` instead, do the `delete` **before** the first `git-sync export` so the failed entity never lands in git history. Order matters: agents reflex to "export to checkpoint progress," but for transforms an export of a soft-failed state is mostly noise that needs a follow-up cleanup commit. See the `git-sync` skill, "Read state before mutating" for the ordering rule.
209
+
210
+ ## Drop the materialized table (keep the transform)
211
+
212
+ ```bash
213
+ mb transform delete-table <id> --yes --profile <name>
214
+ ```
215
+
216
+ Useful when you've changed the SELECT and want a fresh `CREATE TABLE` on the next run. **`--yes` is required** in non-interactive contexts; without it the command exits with `--yes required to delete non-interactively`.
217
+
218
+ ## Delete the transform
219
+
220
+ ```bash
221
+ mb transform delete <id> --yes --profile <name>
222
+ ```
223
+
224
+ Removes the definition. Whether the materialized table is dropped depends on the server — check with `mb table list --db-id <db-id> --profile <name> --json` if it matters. Same `--yes` rule as `delete-table`.
225
+
226
+ ## Transform jobs (schedules)
227
+
228
+ A schedule lives in a separate resource (`transform-job`) and references one or more transform ids. Create with the same body-input pattern (`--file body.json`); see `mb transform-job --help` for the verb list. Most ad-hoc agent work is one-off `transform run`, not job authoring.
229
+
230
+ ## Don't (transform-specific)
231
+
232
+ - Don't put `transform run` calls in tight polling loops — pass `--wait` and let the CLI handle the polling. Manual loops without `--wait` will hammer the server.
233
+ - Don't author MBQL 4 (the legacy nested `{ type: "query", query: {...} }` shape) by hand — pull a sample with `mb transform get <id> --full --json`. MBQL 5 (`lib/type: "mbql/query"`) **is** authorable by hand thanks to the `mb query --print-schema` + `--dry-run` feedback loop; for non-trivial pipelines you may still prefer building in the UI and exporting.
234
+ - Don't write the workspace isolation schema into `target.schema` or SQL. See the `workspace` skill for the canonical-name rule.
235
+ - Don't paste a `transform get` body into `transform update` — the PUT endpoint only accepts writable keys, and unknown keys (notably `tags`, `source_type`, `entity_id`, `created_at`, `last_run`) leak as raw SQL errors. See "Update body: send only writable keys" above. Use `tag_ids` (not `tags`) on the REST contract.