@metabase/cli 0.1.16 → 0.1.17

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 (216) hide show
  1. package/README.md +21 -0
  2. package/dist/{add-collection-H4LcP-9B.mjs → add-collection-C-t9SQBk.mjs} +5 -5
  3. package/dist/add-collection-Dek6kiRI.mjs +11 -0
  4. package/dist/{archive-BCXoM1nX.mjs → archive-C7dnyzVY.mjs} +9 -7
  5. package/dist/{archive-D1mfiftv.mjs → archive-CaoUUTIb.mjs} +8 -7
  6. package/dist/{archive-CQQWkC5S.mjs → archive-D1FX-sbU.mjs} +7 -6
  7. package/dist/{archive-hN8PfvhX.mjs → archive-DS-KEB4a.mjs} +7 -6
  8. package/dist/{archive-CdUS-OG9.mjs → archive-LG8u7ec5.mjs} +7 -6
  9. package/dist/{archive-C6MDtV1F.mjs → archive-qSOcACQo.mjs} +8 -6
  10. package/dist/{archive-B3cjzOIK.mjs → archive-wNXwIiF0.mjs} +8 -7
  11. package/dist/auth-Kv2MRkRk.mjs +22 -0
  12. package/dist/{body-DB2upz6a.mjs → body-IcJ5kFtk.mjs} +3 -3
  13. package/dist/{branches-B6R60Vr1.mjs → branches-BlNCTmFB.mjs} +7 -6
  14. package/dist/{cancel-oPsWomYc.mjs → cancel-BnveTPNw.mjs} +6 -5
  15. package/dist/{cancel-task-CleJVDNI.mjs → cancel-task-CSVI8Zgl.mjs} +7 -6
  16. package/dist/{capabilities-BX1rnVuH.mjs → capabilities-N0jo5U7S.mjs} +1 -1
  17. package/dist/card-Bp58WEUF.mjs +26 -0
  18. package/dist/{card-wCPcuKSi.mjs → card-DEmcRlNO.mjs} +6 -5
  19. package/dist/{cards-Bw37jizL.mjs → cards-CMgGA8OZ.mjs} +8 -6
  20. package/dist/cli.mjs +54 -27
  21. package/dist/collection-D8TI20Ir.mjs +23 -0
  22. package/dist/{collection-namespace-CUDPh2rF.mjs → collection-namespace-CsaxEqOb.mjs} +2 -2
  23. package/dist/command-augment-DdZIfx1V.mjs +11 -0
  24. package/dist/{create-Bj6PuaAW.mjs → create-AeWNv0v-.mjs} +9 -8
  25. package/dist/{create-CZ0_s5ap.mjs → create-B1xfaZJB.mjs} +7 -6
  26. package/dist/{create-B87ZQjNM.mjs → create-B6LQutd0.mjs} +17 -12
  27. package/dist/{create-CF2Zn4pT.mjs → create-B79M8YpX.mjs} +10 -9
  28. package/dist/{create-Cy2TNnJB.mjs → create-Bg_uMu0p.mjs} +9 -8
  29. package/dist/{create-DSWjS09p.mjs → create-BpkWlgoR.mjs} +18 -12
  30. package/dist/{create-D-qE3Oq8.mjs → create-C7u3umLj.mjs} +9 -8
  31. package/dist/{create-B-mvVFIl.mjs → create-FZOrCw5k.mjs} +21 -12
  32. package/dist/{create-CfLaBO0V.mjs → create-I5Cv-MBa.mjs} +16 -11
  33. package/dist/{create-BEkuqChu.mjs → create-LHqwcVbS.mjs} +16 -11
  34. package/dist/{create-DwoqFYb9.mjs → create-SHKlj0z3.mjs} +9 -8
  35. package/dist/{create-branch-BaZ00MIc.mjs → create-branch-CGyT99Ny.mjs} +7 -6
  36. package/dist/{current-task-Dr5dOD4V.mjs → current-task-C-0hw2Ae.mjs} +7 -6
  37. package/dist/dashboard-C9pQCnX6.mjs +28 -0
  38. package/dist/{dashboard-B4bn3z6t.mjs → dashboard-DOplbKyQ.mjs} +7 -5
  39. package/dist/{database-BJxGUXhA.mjs → database-D9fftP-i.mjs} +1 -1
  40. package/dist/db-DWykmBOh.mjs +28 -0
  41. package/dist/{delete-DHrFA1SZ.mjs → delete--NYYN6wv.mjs} +8 -7
  42. package/dist/{delete-evwMw6Hk.mjs → delete-Do3nn9sl.mjs} +8 -7
  43. package/dist/{delete-runtime-CZMw_AGX.mjs → delete-runtime-B0ha5QR4.mjs} +3 -3
  44. package/dist/{delete-DJti0TOA.mjs → delete-tCVrYjKD.mjs} +8 -7
  45. package/dist/{delete-table-DvuuwVqu.mjs → delete-table-CWSGPw0i.mjs} +8 -7
  46. package/dist/{dependencies-DrV31Rj5.mjs → dependencies-CyFocD8I.mjs} +7 -6
  47. package/dist/{dirty-C-rkrnVM.mjs → dirty-BXM0YJ_a.mjs} +7 -6
  48. package/dist/document-DNDm8_Py.mjs +22 -0
  49. package/dist/{eid-BeXI-eII.mjs → eid-p-zn_1RJ.mjs} +13 -7
  50. package/dist/{error-CIObEXLY.mjs → error-5H_tcfL8.mjs} +2 -2
  51. package/dist/{export-CGa-SgEV.mjs → export-B_8wghyo.mjs} +9 -8
  52. package/dist/field-CFt1-KDR.mjs +21 -0
  53. package/dist/{fields-Recymu7n.mjs → fields-BsufL9kv.mjs} +8 -7
  54. package/dist/{get-rFcAVIch.mjs → get-4lufgahL.mjs} +7 -6
  55. package/dist/{get-BQxLtFE-.mjs → get-68P-L4ja.mjs} +7 -6
  56. package/dist/{get-Nc5GOs6-.mjs → get-B45uYMAs.mjs} +8 -6
  57. package/dist/{get-b4xbfFbA.mjs → get-B67pe00Z.mjs} +7 -6
  58. package/dist/{get-BAH_M5vj.mjs → get-BJoZkATD.mjs} +7 -6
  59. package/dist/{get-DaixPDU9.mjs → get-BTKeO4vE.mjs} +7 -6
  60. package/dist/{get-CYh2DBsq.mjs → get-C7mq17-2.mjs} +7 -6
  61. package/dist/{get-CHi8tU_1.mjs → get-CDLa6LiC.mjs} +9 -8
  62. package/dist/{get-CARdLkmp.mjs → get-CPw0ROO4.mjs} +7 -6
  63. package/dist/{get-CXMv-r1p.mjs → get-Cl5Ak73C.mjs} +9 -7
  64. package/dist/{get-BM-d3-zk.mjs → get-D3k2JUsa.mjs} +7 -6
  65. package/dist/{get-7fWSU6ow.mjs → get-DTqBuIf3.mjs} +6 -5
  66. package/dist/{get-Q2WZ79q_.mjs → get-DeLnNVQE.mjs} +8 -7
  67. package/dist/{get-C6g2C4dM.mjs → get-Df_3LCVC.mjs} +7 -6
  68. package/dist/{get-BD_P_Ejc.mjs → get-DurjkUKJ.mjs} +7 -6
  69. package/dist/{get-run-CfQR6ZNa.mjs → get-run-Dq4qfGfD.mjs} +7 -6
  70. package/dist/git-sync-ByvjqSiy.mjs +31 -0
  71. package/dist/group-BNE_RiH5.mjs +28 -0
  72. package/dist/{has-remote-changes-Bvyv4FLP.mjs → has-remote-changes-E-4N_O_t.mjs} +7 -6
  73. package/dist/{import-c3o3OAx0.mjs → import-Dg0j3HCP.mjs} +9 -8
  74. package/dist/{input-BXWgdKiS.mjs → input-7Sj85_K7.mjs} +1 -1
  75. package/dist/is-dirty-D7UMN0mt.mjs +10 -0
  76. package/dist/{is-dirty-BE53XwOC.mjs → is-dirty-DWw1yBeR.mjs} +4 -4
  77. package/dist/{items-1KkBMiO4.mjs → items-CQtt9X4M.mjs} +9 -8
  78. package/dist/{key-DwiMOWRQ.mjs → key-bltP32Pm.mjs} +1 -1
  79. package/dist/library-B5AvpACG.mjs +24 -0
  80. package/dist/{list-C4KnM3Rq.mjs → list-B1uVWy6A.mjs} +8 -6
  81. package/dist/{list-DPcPTqFU.mjs → list-BAOTQHst.mjs} +6 -5
  82. package/dist/{list-D52_BozQ.mjs → list-BF-W4jOZ.mjs} +9 -7
  83. package/dist/{list-7rwzxX6t.mjs → list-BFVuPodI.mjs} +6 -5
  84. package/dist/{list-DHb4vQUM.mjs → list-BHAWYZ1z.mjs} +6 -5
  85. package/dist/{list-BqgbrpQQ.mjs → list-BKUvhAs4.mjs} +6 -5
  86. package/dist/{list-CO5J3SZU.mjs → list-BgUWqZa2.mjs} +8 -7
  87. package/dist/{list-F0vkE22V.mjs → list-C4bALfCs.mjs} +7 -6
  88. package/dist/{list-Drr4JiWg.mjs → list-C9O2RY5u.mjs} +6 -5
  89. package/dist/{list-CSjFJDls.mjs → list-CfqpDTna.mjs} +6 -5
  90. package/dist/{list-FR8Q1SzV.mjs → list-CvhVYOVl.mjs} +7 -6
  91. package/dist/{list-BC2B02IR.mjs → list-D3TSAqwl.mjs} +6 -5
  92. package/dist/{list-BwdO1_gX.mjs → list-D5Gdz8hi.mjs} +6 -5
  93. package/dist/{list-DUEYX3bX.mjs → list-DG0FIhTK.mjs} +6 -5
  94. package/dist/{list-DIvOPW1g.mjs → list-KxQNqp4T.mjs} +8 -7
  95. package/dist/{login-C0Rf2hg0.mjs → login-Bt6j6yBM.mjs} +10 -9
  96. package/dist/{logout-C7_UON-s.mjs → logout-ANkL02p0.mjs} +6 -5
  97. package/dist/{manifest-B2F8iL7X.mjs → manifest-BVf8P4bl.mjs} +9 -2
  98. package/dist/measure-CgFwtL1r.mjs +25 -0
  99. package/dist/{metadata-Db3Kpo-z.mjs → metadata-ClehtgZj.mjs} +9 -8
  100. package/dist/{metadata-FltZq5Ek.mjs → metadata-DplshwI3.mjs} +8 -7
  101. package/dist/{command-augment-CAur0XOQ.mjs → notice-DyVl5aYB.mjs} +1 -11
  102. package/dist/parameter-CiJ4CwWE.mjs +118 -0
  103. package/dist/parameter-values-D4J8Ctu0.mjs +56 -0
  104. package/dist/{parse-enum-BatHQ-Gs.mjs → parse-enum-BL9i_brN.mjs} +1 -1
  105. package/dist/{parse-id-B5adfBlS.mjs → parse-id-D4LeTUsP.mjs} +1 -1
  106. package/dist/{parse-ref-CZr1bYIl.mjs → parse-ref-CB_KvF9h.mjs} +1 -1
  107. package/dist/{path-BojuJkE4.mjs → path-5nQgdvrs.mjs} +6 -5
  108. package/dist/{poll-AduuU55-.mjs → poll-BpAJpvb-.mjs} +2 -2
  109. package/dist/{poll-task-B00Qwd87.mjs → poll-task-TilgciQn.mjs} +2 -2
  110. package/dist/{preflight-QVPvG_Xg.mjs → preflight-OfHU3Toi.mjs} +4 -4
  111. package/dist/{process-DsGf7Mg5.mjs → process-j8UHMHc2.mjs} +1 -1
  112. package/dist/{prompt-Bc_bHSD0.mjs → prompt-C85xd9HR.mjs} +1 -1
  113. package/dist/{publish-h5RJh6im.mjs → publish-DKiqyDwo.mjs} +9 -8
  114. package/dist/{query-Dvi-Rksy.mjs → query-BWJ5h1g3.mjs} +19 -13
  115. package/dist/{query-BUkuB4bZ.mjs → query-CQ3xXa9P.mjs} +10 -8
  116. package/dist/{query-result-L5_NrwQR.mjs → query-result-D6mfoVfQ.mjs} +1 -1
  117. package/dist/{remove-collection-D8ZfB2RN.mjs → remove-collection-CO-aMzTO.mjs} +9 -8
  118. package/dist/{rescan-values-DOsDLrRG.mjs → rescan-values-DpL6LGAK.mjs} +9 -8
  119. package/dist/{resolve-BQ9vjlNJ.mjs → resolve-Dj2MTBkn.mjs} +1 -1
  120. package/dist/{run-CeG0KH5W.mjs → run-DUcdaZs3.mjs} +6 -5
  121. package/dist/{run-CjhD-Zbr.mjs → run-QYJ-mAaG.mjs} +9 -8
  122. package/dist/{runs-6k8C6kXF.mjs → runs-DgasTnGd.mjs} +8 -7
  123. package/dist/{runtime-CmAIahm5.mjs → runtime-BJtuxWM8.mjs} +5 -3
  124. package/dist/{schema-tables-C45QegaY.mjs → schema-tables-ooYjimrV.mjs} +8 -7
  125. package/dist/{schemas-EVwEFuTj.mjs → schemas-HjFPzsd-.mjs} +6 -5
  126. package/dist/{search-C_uw_D1U.mjs → search-vaT5EwhG.mjs} +11 -5
  127. package/dist/segment-BPC725mo.mjs +25 -0
  128. package/dist/{selectors-AktxTEMK.mjs → selectors-DlmZpo2L.mjs} +3 -3
  129. package/dist/{set-C3EAuyb8.mjs → set-CLHJauzG.mjs} +9 -8
  130. package/dist/{set-active-BW6LN6y0.mjs → set-active-oZOUe9V7.mjs} +6 -5
  131. package/dist/setting-DHYO-W5g.mjs +20 -0
  132. package/dist/{setup-CYrbNrlG.mjs → setup-D1de5Sbc.mjs} +8 -7
  133. package/dist/{skills-DJsuBguh.mjs → skills-B6gfH0iR.mjs} +1 -1
  134. package/dist/{skills-D6xQkmhu.mjs → skills-NhgVypJ7.mjs} +3 -3
  135. package/dist/snippet-DtLmHr2J.mjs +22 -0
  136. package/dist/{stash-C1V2FvJR.mjs → stash-DoRwelwY.mjs} +9 -8
  137. package/dist/{status-CxYw6zQM.mjs → status-BpMOlfsA.mjs} +8 -7
  138. package/dist/{status-DY92F9mn.mjs → status-CmRgHTrV.mjs} +6 -5
  139. package/dist/{summary-DOxgqJoA.mjs → summary-BEu7pmpq.mjs} +7 -6
  140. package/dist/{sync-schema-CQPfffjU.mjs → sync-schema-DTUXubMg.mjs} +11 -10
  141. package/dist/table-B1iBqmNu.mjs +22 -0
  142. package/dist/{table-CDMG0Zi5.mjs → table-DE3i82T_.mjs} +1 -1
  143. package/dist/transform-BuIooRQh.mjs +31 -0
  144. package/dist/transform-job-C3yz0krA.mjs +25 -0
  145. package/dist/transform-tag-CJD6mJUe.mjs +21 -0
  146. package/dist/{transforms-DzBJDydn.mjs → transforms-CWvMpLc9.mjs} +7 -6
  147. package/dist/{tree-B3f5F_dP.mjs → tree-D18vYSe4.mjs} +6 -5
  148. package/dist/{unpublish-B5RDeN-V.mjs → unpublish-CJbL4ZsC.mjs} +7 -6
  149. package/dist/{update-DSueNZRw.mjs → update-7DacwIvi.mjs} +10 -9
  150. package/dist/{update-D5gioyBa.mjs → update-BjjZIe2W.mjs} +10 -9
  151. package/dist/{update-C0pFSc1B.mjs → update-Bos8nnv0.mjs} +18 -13
  152. package/dist/{update-PZPNx0Xd.mjs → update-BvsvyBw9.mjs} +17 -12
  153. package/dist/{update-DudyZ-FP.mjs → update-CSxwZ2us.mjs} +11 -10
  154. package/dist/{update-CiWPEqQ-.mjs → update-Ck0Kxv8u.mjs} +10 -9
  155. package/dist/{update-D698CaeV.mjs → update-DV7IY7IQ.mjs} +10 -9
  156. package/dist/{update-DOfL_KPx.mjs → update-DhOfrW1j.mjs} +19 -13
  157. package/dist/{update-BIZ9XhjS.mjs → update-Doia9MP_.mjs} +17 -12
  158. package/dist/{update-I3TA2Tem.mjs → update-Dxa_6H2A.mjs} +22 -13
  159. package/dist/{update-dashcard-BXZ4vS15.mjs → update-dashcard-CZUWZhal.mjs} +11 -9
  160. package/dist/{update-CcvDVqNd.mjs → update-mr9todHq.mjs} +10 -9
  161. package/dist/{upgrade-4cWfLu90.mjs → upgrade-CBW8V9qZ.mjs} +7 -6
  162. package/dist/{uuid---pAboNQ.mjs → uuid-D6JVJ-R1.mjs} +10 -5
  163. package/dist/{validate-VawhJ5Sc.mjs → validate-BqNW4Sk1.mjs} +2 -2
  164. package/dist/{validate-query-CSV-TTnd.mjs → validate-query-CcZVKYPV.mjs} +3 -3
  165. package/dist/{values-I503dI7K.mjs → values-DBvuGnnA.mjs} +7 -6
  166. package/dist/{verify-BMhTWW9s.mjs → verify-LIShMNZ2.mjs} +2 -2
  167. package/dist/{wait-oMSs_IdS.mjs → wait-DUHze3_B.mjs} +8 -7
  168. package/dist/{wait-flags-HtCL2l1r.mjs → wait-flags-Ybpt98PK.mjs} +2 -2
  169. package/package.json +1 -1
  170. package/skill-data/core/SKILL.md +44 -56
  171. package/skill-data/data-workflow/SKILL.md +116 -0
  172. package/skill-data/{data-analysis/SKILL.md → data-workflow/references/answering-questions.md} +7 -13
  173. package/skill-data/{data-transformation/SKILL.md → data-workflow/references/building-clean-tables.md} +23 -32
  174. package/skill-data/{semantic-layer/SKILL.md → data-workflow/references/reusable-definitions.md} +26 -56
  175. package/skill-data/document/SKILL.md +10 -20
  176. package/skill-data/git-sync/SKILL.md +36 -36
  177. package/skill-data/mbql/SKILL.md +3 -15
  178. package/skill-data/mbql/references/operators.md +9 -0
  179. package/skill-data/transform/SKILL.md +26 -42
  180. package/skill-data/visualization/SKILL.md +6 -6
  181. package/skill-data/visualization/references/settings.md +12 -0
  182. package/skills/metabase-cli/SKILL.md +2 -2
  183. package/dist/add-collection-BqfYL4FU.mjs +0 -10
  184. package/dist/auth-BaCMFLTA.mjs +0 -19
  185. package/dist/card-DzH3aK0a.mjs +0 -20
  186. package/dist/collection-Wagz-ira.mjs +0 -20
  187. package/dist/dashboard-OUgS1Gi-.mjs +0 -21
  188. package/dist/db-Btfl5JMZ.mjs +0 -22
  189. package/dist/document-CU28GfFw.mjs +0 -19
  190. package/dist/field-BTbzlcyC.mjs +0 -18
  191. package/dist/git-sync-CBxS2urR.mjs +0 -28
  192. package/dist/is-dirty-Z-pqyVyB.mjs +0 -9
  193. package/dist/library-BlbH0xyK.mjs +0 -18
  194. package/dist/measure-BUedPu4K.mjs +0 -19
  195. package/dist/segment-CkZUZcWz.mjs +0 -19
  196. package/dist/setting-C50HEiGG.mjs +0 -17
  197. package/dist/snippet-BjaWAxCu.mjs +0 -19
  198. package/dist/table-B35ovbcd.mjs +0 -19
  199. package/dist/transform-DF79sJ0_.mjs +0 -25
  200. package/dist/transform-job-PmA_D8gz.mjs +0 -22
  201. package/dist/transform-tag-CE3cuO1K.mjs +0 -18
  202. package/skill-data/robot-data-engineer/SKILL.md +0 -142
  203. /package/dist/{body-flags-D7q87Btw.mjs → body-flags-DWTTxJpP.mjs} +0 -0
  204. /package/dist/{collection-Deiziuu2.mjs → collection-DrLpA1SO.mjs} +0 -0
  205. /package/dist/{document-qfwR0r63.mjs → document-1W7NRaO_.mjs} +0 -0
  206. /package/dist/{field-E0IBy4Uw.mjs → field-CMY_LWUe.mjs} +0 -0
  207. /package/dist/{measure-BCv5wDDN.mjs → measure-DoJvtCaA.mjs} +0 -0
  208. /package/dist/{paginate-BexjkjbY.mjs → paginate-FVZUxL4J.mjs} +0 -0
  209. /package/dist/{render-CkuFkWlQ.mjs → render-BTKnWL0d.mjs} +0 -0
  210. /package/dist/{revision-message-flag-CP5NFrWQ.mjs → revision-message-flag-CHrJgFFx.mjs} +0 -0
  211. /package/dist/{segment-BAUuELKs.mjs → segment-TXktTCfU.mjs} +0 -0
  212. /package/dist/{setting-DhMk0TNo.mjs → setting-m46MUtW5.mjs} +0 -0
  213. /package/dist/{snippet-D4SyVLKB.mjs → snippet-CtA2Pkoa.mjs} +0 -0
  214. /package/dist/{transform-MmqHKGU-.mjs → transform-DEF38FWe.mjs} +0 -0
  215. /package/dist/{transform-job-CtVziW85.mjs → transform-job-CtixL4An.mjs} +0 -0
  216. /package/dist/{transform-tag-wFiWmiyO.mjs → transform-tag-rsIrckCM.mjs} +0 -0
@@ -1,36 +1,27 @@
1
- ---
2
- name: data-transformation
3
- description: Turn a raw, normalized source database into a small set of clean, analysis-ready tables. Claude investigates the source, works out the real-world "things" the data is about (even when each one is scattered across several tables), decodes coded/JSON/translated values into readable text, and builds one wide, denormalized table per thing as Metabase transforms. Designed for a non-technical user who knows their domain. Use whenever someone wants to "clean up", "flatten", "denormalize", "make sense of", or "build analysis-ready tables from" a raw database. This is the strategy skill for modeling a whole database into a set of clean tables; for authoring or running one individual transform (body shape, flags, run inspection), use the `transform` skill instead.
4
- allowed-tools: Read, Write, Edit, Bash, AskUserQuestion, EnterPlanMode, ExitPlanMode
5
- ---
1
+ # Build clean tables
6
2
 
7
- # Data Transformation
3
+ > Part of the **`data-workflow`** skill — the "build clean tables" stage. It assumes that skill's **Shared Contract** (how to communicate, PII, autonomy, permission-denied) and final-recap rule. CLI mechanics: `core` (auth, `field`/`table` verbs, library publish), `mbql` (transform query bodies), `transform` (creating/running transforms).
8
4
 
9
- > **Shared contract (read first).** This skill is part of the `robot-data-engineer` family and follows its shared rules: ask before showing PII row-by-row (names, emails, phones) — default to aggregates; when asked for something the CLI can't do (alerts, dashboard filters), name the limit instead of erroring into raw SQL; honor the autonomy mode the user picked. The jargon rules are spelled out below (**Who you're talking to**). Full contract and the autonomy slider live in the router — run `mb skills get robot-data-engineer` and read its **Shared Contract**.
5
+ **Contents**
6
+
7
+ - [Two kinds of decisions](#two-kinds-of-decisions)
8
+ - [The process](#the-process) — [Phase 0 — Get Oriented](#phase-0--get-oriented), [Phase 1 — Investigate](#phase-1--investigate-in-plan-mode-if-they-choose), [Phase 2 — Present what you found](#phase-2--present-what-you-found-plain-language), [Phase 3 — Iterate](#phase-3--iterate), [Phase 4 — Build, check, hand back](#phase-4--build-check-hand-back)
9
+ - [A worked decode example](#a-worked-decode-example-for-your-reference-not-the-users)
10
+ - [Cleaning checklist](#cleaning-checklist-for-your-reference-not-the-users)
10
11
 
11
12
  Your job: take a raw source database — usually normalized, often synced from a SaaS tool by a connector like Fivetran or Airbyte — and produce a **small set of wide, clean, analysis-ready tables**, one per real-world _thing_ the data is about, built as Metabase **transforms** the user can inspect.
12
13
 
13
14
  Drive everything through the `mb` CLI. Load the skills you'll need:
14
15
 
15
16
  ```bash
16
- mb skills get core # auth, profiles, db/table/field inspection, query
17
+ mb skills get core # auth, profiles, db/table/field inspection, query, library publish
17
18
  mb skills get mbql # if you build transform queries in MBQL
18
19
  mb skills get transform # creating/running transforms, run inspection
19
20
  ```
20
21
 
21
- Users authenticate. Pick the profile per `core`'s **Auth & profiles** and pass `--profile <name>` to every command. That profile's `url` is the instance's base URL; the browser links below are built from it.
22
-
23
- ---
24
-
25
- ## Who you're talking to
26
-
27
- A **non-technical user who knows their domain well** — they understand the business (events, customers, invoices, etc.) but not databases.
22
+ Pick the profile per `core`'s **Auth & profiles** and pass `--profile <name>` to every command. That profile's `url` is the instance's base URL; the browser links below are built from it.
28
23
 
29
- - **No modeling jargon.** Skip warehouse vocabulary — grain, fact/dimension table, normalize, surrogate key, entity, materialize — prefer plain phrasing: "one row per \_\_\_", "what it tells you", "links up with", "how full a column is". **But don't overdo it:** basic relational terms are fine — table, column, ERD, schema, key, foreign key (cardinality too, though "one-to-many" usually lands better). **Metabase's product terms are encouraged** — Question, Model, Segment, Measure, Metric, Transform — they're not database jargon.
30
- - **Don't lean on raw SQL to communicate.** They may follow a simple `SELECT`, but don't explain work via SQL or ask them to read/write it.
31
- - Group what you show by **the question a column answers**, never by which source table it came from.
32
- - Be a **helpful assistant, not an engineer reporting status.** Elide machinery; ask sharp questions that matter.
33
- - **If you ever ask the user a question, wait for their answer.** They may say "go" and come back later.
24
+ Two communication habits specific to this work, on top of the Shared Contract: **don't communicate through SQL** — they may follow a simple `SELECT`, but never explain your work via SQL or ask them to read or write it; and **group what you show by the question a column answers**, never by which source table it came from. Be a helpful assistant, not an engineer reporting status — elide the machinery, ask the sharp questions that matter.
34
25
 
35
26
  ---
36
27
 
@@ -42,7 +33,7 @@ Sort every choice into one of these.
42
33
 
43
34
  1. Never flatten multi-valued fields into opaque blobs (e.g. three options squished: `"email | phone | text"`). It destroys filterability (the whole point).
44
35
  2. Never use jargon with the user. Explain by domain and telos.
45
- 3. Always surface **real data you're about to leave out** proactively, ranked by how much is extant.
36
+ 3. Always surface **real data you're about to leave out** proactively, ranked by how much is extant. (Phase 2(c) is where you present it.)
46
37
  4. Never guess what schema mean from their name alone. Confirm against actual values, interpret them in context: the table the field belongs to and the relevant domain (e.g., a status on orders ≠ status on subscriptions).
47
38
  5. Never silently drop a whole _thing_. Dropping a column is routine; dropping a whole kind-of-thing (e.g. "suppliers") must be surfaced and confirmed.
48
39
  6. Never drop columns that link things together. Every table keeps its own id **and** the ids tying it to other tables — alongside the readable labels you copy in, not instead of (label for reading, id for joining). You're building tables about _related_ things, so they **will** be combined ("sales per region", "messages per customer") — dropped ids make that quietly impossible. Keep the ids.
@@ -68,7 +59,7 @@ Phrase a prudential call as a lean plus a nod:
68
59
 
69
60
  ### Phase 0 — Get Oriented
70
61
 
71
- **Pin down where the data lives — ask before you hunt.** A table or schema name the user mentions tells you _what_ but not _where_: an instance can hold several databases, each with several schemas. Rather than listing them all to find it, just ask — "Which database is this in, and the schema if you know it? No worries if not — I can find it." A confident answer short-circuits a lot of blind searching; "not sure" costs nothing and you fall back to locating it yourself. If you've genuinely looked and still can't find a table the user is sure is there, don't keep digging — one likely reason is Metabase hasn't synced that database's latest schema; gently raise it and let the user run the sync from Metabase.
62
+ **Pin down where the data lives — ask before you hunt.** A table or schema name the user mentions tells you _what_ but not _where_: an instance can hold several databases, each with several schemas. Rather than listing them all to find it, just ask — "Which database is this in, and the schema if you know it? No worries if not — I can find it." A confident answer short-circuits a lot of blind searching; "not sure" costs nothing and you fall back to locating it yourself (use `core`'s narrowest-first crawl ladder). If you've genuinely looked and still can't find a table the user is sure is there, don't keep digging — one likely reason is Metabase hasn't synced that database's latest schema; gently raise it and let the user run the sync from Metabase.
72
63
 
73
64
  As soon as you know which database and schema you're in:
74
65
 
@@ -87,7 +78,7 @@ Orientation done, you're about to go heads-down. First, offer two ways to work:
87
78
 
88
79
  First path: **enter plan mode** (`EnterPlanMode`). Everything up to the agreed table list — investigate, present, prudential calls, naming (Phases 1–3) — happens inside it, read-only; you exit once, at the approval gate before building (Phase 4). Second path: skip it, shape it conversationally through the same phases. Either way, don't build until the design is settled and approved.
89
80
 
90
- Plan mode is a long quiet stretch. So whenever you surface — a question now, the plan at the end — **carry your own context**: recap what it rests on right before you ask, never a back-reference to something said while they were away.
81
+ Plan mode is a long quiet stretch. So whenever you surface — a question now, the plan at the end — **carry your own context**: recap what it rests on right before you ask, never a back-reference to something said while they were away. And whenever you ask a question, **wait for their answer** — they may say "go" and come back later.
91
82
 
92
83
  Then dig in. Don't narrate this — a single "Let me take a look at what's in here — one minute" is enough. Keep it cheap: never pull whole-warehouse rollups (they blow up); use compact column listings, `LIMIT`/sample queries, and `GROUP BY count(*)`.
93
84
 
@@ -109,11 +100,11 @@ Three things, in order:
109
100
 
110
101
  > **Customers** — one row per customer. Who they are (name, company, location), how they've been in touch, what they've spent, whether they're active or churned.
111
102
 
112
- **(b) The full inventory — including what you'd leave out.** Never infer scope silently:
103
+ **(b) The full inventory — including what you'd leave out.** Never infer scope silently (rule 5):
113
104
 
114
105
  > I found 6 kinds of things: **Customers, Orders, Products, Suppliers, Shipments, Returns.** I'd build the first four. **Shipments** and **Returns** also have real data — want those in, or leave them?
115
106
 
116
- **(c) What would be set aside — proactively, ranked, two buckets:**
107
+ **(c) What would be set aside.** This is rule 3 made concrete — proactively, ranked by how much is extant, in two buckets:
117
108
 
118
109
  > Nothing important is lost. A few things set aside:
119
110
  > • **Real data** — gift-message text (6 of 10 orders), delivery instructions (most), preferred carrier. Minor, but real — want any kept?
@@ -127,7 +118,7 @@ Cheap, because nothing's built. Adjust the set of things, what's kept, and the s
127
118
 
128
119
  ### Phase 4 — Build, check, hand back
129
120
 
130
- Design settled — now you build, the first step that writes; plan mode, if you used it, is behind you. Build one wide transform per agreed thing, for how it'll be judged: output that's readable on sight, not just one that runs clean. Each table:
121
+ Design settled — now you build, the first step that writes; plan mode, if you used it, is behind you. Build one wide transform per agreed thing (transform body shape, create, run-with-wait — see `transform`), for how it'll be judged: output that's readable on sight, not just one that runs clean. Each table:
131
122
 
132
123
  - **Denormalized, but the link stays.** Copy in related context so casual reading needs no lookups (a product's name and price on the orders table) — **and keep the linking id beside it** (the product's id too, per rule 6). Use the same id name everywhere a thing appears.
133
124
  - **Decoded**: codes and JSON become readable text; bookkeeping columns and soft-deleted rows are gone (filter the source's soft-delete flag — Fivetran's `_fivetran_deleted`, Airbyte's `_ab_cdc_deleted_at`, or a plain `deleted_at`/`is_deleted` — so tombstones never reach clean data).
@@ -137,13 +128,13 @@ Design settled — now you build, the first step that writes; plan mode, if you
137
128
 
138
129
  Then make the links real, not just implied:
139
130
 
140
- - **Wire foreign keys between your tables.** Mark each linking id as a foreign key pointing at the id it references (`mb field update` — set the column's type to foreign-key and its target). Now Metabase itself knows the tables connect and can traverse them.
131
+ - **Wire foreign keys between your tables.** Mark each linking id as a foreign key pointing at the id it references — set the column's type to foreign-key and its target so Metabase itself knows the tables connect and can traverse them.
141
132
  - **Graft onto existing clean data** the user approved (step 3 / Phase 1): point the linking id at the existing table's id the same way. Link, don't duplicate.
142
133
 
143
- **Set the metadata — a transform's output starts blank, and these tables are Library-bound.** A fresh transform table has no descriptions, raw column names, and untyped columns. You worked it out while investigating; don't leave that knowledge stranded in this chat. Set it on the table so the data explains itself inside Metabase (search, the Question editor, Metabot) and is fit to publish:
134
+ **Set the metadata — a transform's output starts blank, and these tables are Library-bound.** A fresh transform table has no descriptions, raw column names, and untyped columns. You worked it out while investigating; don't leave that knowledge stranded in this chat. Set it on the table so the data explains itself inside Metabase (search, the Question editor, Metabot) and is fit to publish. The mechanics — `mb field update` for semantic types / FK targets / display names, `mb table update` for table descriptions — and their footguns are in `core`; the calls about _what_ to set:
144
135
 
145
- - **Semantic types — the highest-value piece.** A column's semantic type is what makes Metabase treat it right: `type/Email`, `type/Currency`/`type/Price`, `type/Category` (turns into a filter dropdown), `type/City`/`type/State`/`type/Country`, `type/CreationTimestamp`, `type/Description`. Set it on every column whose meaning you decoded — `mb field update <id> --body '{"semantic_type":"type/Currency"}'`. A typed column shows money as money, offers a filter dropdown, and lands on the right chart axis for everyone downstream; an untyped one is a guess.
146
- - **Descriptions.** A one-line description on each table and every non-obvious column (`mb table update` / `mb field update`).
136
+ - **Semantic types — the highest-value piece.** A column's semantic type is what makes Metabase treat it right: `type/Email`, `type/Currency`/`type/Price`, `type/Category` (turns into a filter dropdown), `type/City`/`type/State`/`type/Country`, `type/CreationTimestamp`, `type/Description`. Set it on every column whose meaning you decoded. A typed column shows money as money, offers a filter dropdown, and lands on the right chart axis for everyone downstream; an untyped one is a guess.
137
+ - **Descriptions.** A one-line description on each table and every non-obvious column.
147
138
  - **Display names.** When a cleaned-up column name still isn't plain English, set a readable `display_name`.
148
139
 
149
140
  When the semantics are **already spelled out** — the user is porting dbt models (the `schema.yml` carries column descriptions and types), or you settled each field's meaning together here — that documentation _is_ the metadata. Carry it straight onto the tables and fields rather than letting it evaporate.
@@ -154,7 +145,7 @@ When refining a built transform _with_ the user, open its inspector so you're lo
154
145
 
155
146
  **Pass 1 — Correctness (did it run right).** After each transform runs, run quick ad-hoc tests against what Phase 0 led you to expect: row counts in the right ballpark, decoded columns readable (no stray codes), linking ids that resolve to the other tables, no column unexpectedly all-null or blown up in count. Treat surprises as bugs to chase, not noise. A table that can't combine with the others — a dropped id, or the same id named two ways — is a silent failure; catch it here.
156
147
 
157
- **Pass 2 — Fitness (is it nice to use).** Correct isn't the bar; _usable_ is. `SELECT * FROM <table> LIMIT 20` and read every column as if you'd never seen the source: would a non-technical person find each one readable? Smells that say not-yet, even though nothing errored:
148
+ **Pass 2 — Fitness (is it nice to use).** Correct isn't the bar; _usable_ is. `SELECT * FROM <table> LIMIT 20` and read every column as if you'd never seen the source: would a business reader find each one readable? Smells that say not-yet, even though nothing errored:
158
149
 
159
150
  - a multi-valued column still a raw JSON/array blob or `["Email","SMS"]` text — rule 1 never actually got resolved;
160
151
  - decoded answers still carrying raw ids with no readable label, or one cryptic column per code;
@@ -174,7 +165,7 @@ Then report plainly:
174
165
 
175
166
  End on that connection map: it's what the user reads to trust the result, and what lets whatever they build next join on the right ids instead of guessing.
176
167
 
177
- These clean tables are exactly what belongs in the **Library** — published tables appear first when anyone picks a data source, so people start from your curated set, not the raw source. If the user wants that, mark them official with `mb library publish --table-ids <ids>` (`mb library create` first if the Library isn't set up; both need the `library` premium feature + admin/data-analyst). Defining reusable segments / measures / metrics on top is the **semantic-layer** skill's job.
168
+ These clean tables are exactly what belongs in the **Library** — published tables appear first when anyone picks a data source, so people start from your curated set, not the raw source. If the user wants that, publish the polished tables to the Library (`mb library publish` / `mb library create` mechanics, premium feature, and permissions are in `core`). Defining reusable segments / measures / metrics on top is the **reusable-definitions** stage (`references/reusable-definitions.md` in this skill).
178
169
 
179
170
  ---
180
171
 
@@ -1,73 +1,47 @@
1
- ---
2
- name: semantic-layer
3
- description: Turn clean, analysis-ready tables into a shared vocabulary the org reuses - Metabase segments (saved filters, e.g. active customers), measures (saved calculations, e.g. net revenue), and metrics (official numbers, e.g. monthly recurring revenue) - so people stop reinventing the same definition five ways. Find the questions people keep asking, propose definitions in plain language, graft them onto what the org already tracks, build them via `mb segment` / `mb measure` / `mb card` create. For a non-technical user who knows their domain. Load when someone wants to "make this reusable", "define X officially", "standardize how we calculate Y", or "create a segment / measure / metric". Strategy skill for designing reusable definitions; for raw `mb segment` / `mb measure` mechanics, use `core`.
4
- allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
5
- ---
1
+ # Define reusable metrics
6
2
 
7
- # Semantic Layer
3
+ > Part of the **`data-workflow`** skill — the "define reusable metrics" stage. It assumes that skill's **Shared Contract** (how to communicate, PII, autonomy, permission-denied) and final-recap rule. CLI mechanics: `core` (the `segment`/`measure` verbs, `revision_message`, library publish), `mbql` (definition bodies).
8
4
 
9
- > **Shared contract (read first).** This skill is part of the `robot-data-engineer` family and follows its shared rules: audience is a non-technical user, so no database jargon (skip "normalize"/"grain"; ERD/foreign key are fine; explain "wide"/"long" the first time you use them). Ask before showing PII row-by-row (names, emails, phones) — default to aggregates. When asked for something the CLI can't do (alerts, dashboard filters), name the limit instead of erroring into raw SQL. Honor the autonomy mode the user picked. Full text and the autonomy slider live in the router — run `mb skills get robot-data-engineer` and read its **Shared Contract** if you haven't.
5
+ - [Autonomy applied here](#autonomy-applied-here)
6
+ - [Two kinds of decisions](#two-kinds-of-decisions)
7
+ - [The process](#the-process) — Phases 0–3
8
+ - [A worked example](#a-worked-example-for-your-reference-not-the-users)
10
9
 
11
10
  Your job: take the clean, analysis-ready tables that already exist and turn the **questions people keep asking** into **shared, reusable definitions** — so "active customer", "net revenue", and "monthly recurring revenue" mean one thing across the whole organization, not five slightly-different things in five people's saved questions.
12
11
 
13
12
  You build three kinds of reusable thing. These are real Metabase features with real names — **use the Metabase names** (segment, measure, metric) and teach them to the user as you go. They're product vocabulary, not jargon. Pair the name with a plain gloss the first time, then use it freely:
14
13
 
15
14
  - **Segment** — a saved filter on a table. A reusable row-selector: "Active customers", "orders over $100", "EU shipments". People pick it from the **Filter** block in the query builder instead of re-typing the conditions. (Docs: <https://www.metabase.com/docs/latest/data-studio/segments>.)
16
- - **Measure** — a saved aggregation on a table. A reusable calculation: "Net Promoter Score", "average order value". People pick it from the **Summarize** block instead of re-writing the formula. Only works on questions built directly on the measure's table. (Docs: <https://www.metabase.com/docs/latest/data-studio/measures>.)
15
+ - **Measure** — a saved aggregation on a table. A reusable calculation: "Net Promoter Score", "average order value". People pick it from the **Summarize** block instead of re-writing the formula. (Docs: <https://www.metabase.com/docs/latest/data-studio/measures>.)
17
16
  - **Metric** — a reusable aggregation that lives in a **collection** (a folder), not bolted to a table. "Monthly recurring revenue", "weekly active users". It's the org's official definition of an important number, can be saved into the **Library**, and can carry a default time dimension for charting. (Docs: <https://www.metabase.com/docs/latest/data-modeling/metrics>.)
18
17
 
19
18
  Introduce each like: _"I'll save this as a **segment** — that's Metabase's word for a reusable filter, so you can pull up active customers with one click anytime."_ After that, just say "segment".
20
19
 
21
- This skill runs **after** the analysis-ready tables exist (build those with transforms — load `mb skills get transform`). Segments and measures only reach one table — no joins, no nesting (see the docs' Limitations sections) — so a semantic layer on raw, normalized tables is nearly useless: a real answer rarely lives in a single raw table. **Wide clean tables first, segments/measures/metrics second.**
20
+ This stage runs **after** the analysis-ready tables exist (make the table wider first — the **build-clean-tables** stage, `references/building-clean-tables.md`; the `transform` skill has the mechanics). Segments and measures only reach one table (hard rule 4 below), so a semantic layer on raw, normalized tables is nearly useless: a real answer rarely lives in a single raw table. **Wide clean tables first, segments/measures/metrics second.**
22
21
 
23
- You drive everything through the `mb` CLI. Load the CLI skills you'll need:
22
+ Load the CLI skills you'll need — `mb skills get core` (auth, profiles, inspection, the `segment`/`measure`/`library` verb mechanics) and `mb skills get mbql` (the definition bodies). Auth and scratch files follow `core`'s recipe: resolve the profile and carry `--profile <name>` into every command.
24
23
 
25
- ```bash
26
- mb skills get core # auth, profiles, db/table/field inspection, query, search
27
- mb skills get mbql # the definition bodies (filters and aggregations) are MBQL 5
28
- ```
24
+ ## Autonomy applied here
29
25
 
30
- Authentication is the user's job. Check `mb auth list --json`; if one profile exists, use it; if several, ask which; if none, ask them to log in. Pass `--profile <name>` to every command.
31
-
32
- ---
33
-
34
- ## Who you're talking to
35
-
36
- A **non-technical user who knows their domain well.** They know the business — who an "active" customer is, what counts as "revenue" — but not databases. So:
37
-
38
- - **Teach the words a curious non-engineer can follow; skip the deep-internals jargon.** Two sets are fine and worth teaching: Metabase product terms (**segment, measure, metric, collection, Library, the Filter / Summarize blocks**) and common data words a domain user can reasonably learn (**table, column, foreign key, schema, join, filter, row**) — gloss them once, then use them. Avoid **deep-internals jargon** that buys nothing for this user: grain, cardinality, normalize/denormalize, surrogate key, MBQL, `table_id`, materialize. Prefer the plain effect when it's clearer ("this number needs data from two tables" reads easier than "this needs a join across two fact tables") — but you don't have to contort around "foreign key" or "schema".
39
- - **Talk about the question, then name the object.** Lead with what it does for them, then attach the term: _"I'll save 'big orders' as a segment so you can pull them up with one click."_ Not a bare "I'll create a segment on `table_id` 235."
40
- - **Be a helpful colleague, not an engineer reporting status.** Elide the wiring (ids, query bodies, the CLI). Ask the one question that actually matters.
41
-
42
- ---
43
-
44
- ## Autonomy — honor the mode the user set
45
-
46
- The user already picked an autonomy mode (the router's Shared Contract asks the slider once, up front — don't re-ask). Apply it to building definitions:
26
+ The user already set an autonomy mode (the `data-workflow` autonomy slider — don't re-ask, don't redefine it). How it lands on building definitions:
47
27
 
48
28
  | Mode | What you do |
49
29
  | ----------------------- | ----------------------------------------------------------------------------------------------------------- |
50
30
  | **Check on everything** | Confirm every single definition (name + plain description) before building it. |
51
- | **Balanced** (default) | Build the obvious ones; ask only on the judgment calls (the prudential list below) and anything ambiguous. |
31
+ | **Balanced** (default) | Build the obvious ones; ask only on the judgment calls (the prudential list) and anything ambiguous. |
52
32
  | **Just go** | Build the whole set, surface judgment calls as "here's what I picked and why — say the word to change any." |
53
33
 
54
- **Two things never bend, in any mode:**
55
-
56
- 1. **When you're genuinely unsure — ask. Never assume.** "Just go" means _decide the obvious_, not _guess on the unclear_. A wrong-but-confident definition of "active customer" is worse than a one-line question.
57
- 2. **The final gate is a hard stop (see Phase 3).** No mode auto-publishes. You always stop, recap in plain language, and hand the user something to eyeball before anything goes live.
58
-
59
- ---
34
+ Two things never bend in any mode: when genuinely unsure, **ask** (the Shared Contract's rule — "Just go" means decide the obvious, not guess on the unclear); and the final gate is a **hard stop** (Phase 3) — no mode auto-publishes.
60
35
 
61
36
  ## Two kinds of decisions
62
37
 
63
38
  **Hard rules — absolutes, never ask:**
64
39
 
65
40
  1. **Never invent what a word means — pin it to real data.** "Active customer" is not yours to define. Before you build a segment for it, find out (from the user, or from how the data actually behaves) what _they_ mean: ordered in the last 90 days? Has a live subscription? Logged in this month? Confirm against actual values, then build to that. A definition built on a guessed meaning is a silent lie everyone then trusts.
66
- 2. **Keep the language at the level set in "Who you're talking to."** Metabase terms and common data words (table, column, foreign key, schema, join) are fine and worth teaching; deep-internals jargon (grain, cardinality, surrogate key, `table_id`) is not.
41
+ 2. **Keep the language at the level the Shared Contract sets.** Metabase terms and common data words (table, column, foreign key, schema, join) are fine and worth teaching; deep-internals jargon (grain, cardinality, surrogate key, `table_id`) is not.
67
42
  3. **Don't bury filters inside measures.** A measure should aggregate _what it's given_; let the user combine it with a segment at question time, rather than welding a filter into the measure. Welded-in filters collide and confuse when someone applies their own filter on top — and the metrics doc explicitly recommends against it. (Use conditional forms like `SumIf`/`CountIf` for "sum only the paid ones" — that's part of the measure's formula, not a hidden row filter.)
68
- 4. **Respect where each thing can reach.** Segments and measures work **only** on a question built _directly_ on their own table — not through a join, not on a question-built-on-a-question (the Limitations sections of both docs say so). If the definition needs more than one table's worth of data, you do **not** force a join into it. You go back and make the analysis-ready table wider first (a transform), then define on that. Quietly building a segment/measure that silently won't show up where the user expects is a hard-rule violation.
69
- 5. **Don't strand a metric on a single data source.** A metric is data-source-bound the same way — defined on table X, it appears only on questions built on table X, not on anything derived from it. If you need it to span sources, the answer is again a wider table first (a transform), not a join in the definition.
70
- 6. **Every definition keeps a clear, plain name and a one-line description in the user's words.** The name is what they'll see in a menu six weeks from now with no memory of this conversation. "Active customers (ordered in last 90 days)" beats "active_seg_v2".
43
+ 4. **Respect where each thing can reach (single-table reach).** Segments and measures work **only** on a question built _directly_ on their own table — not through a join, not on a question-built-on-a-question (the Limitations sections of both docs say so). A metric is data-source-bound the same way: defined on table X, it appears only on questions built on table X, not on anything derived from it. If a definition needs more than one table's worth of data, you do **not** force a join into it — you make the table wider first (the **build-clean-tables** stage, `references/building-clean-tables.md`; the `transform` skill has the mechanics), then define on that. Quietly building a segment/measure/metric that silently won't show up where the user expects is a hard-rule violation.
44
+ 5. **Every definition keeps a clear, plain name and a one-line description in the user's words.** The name is what they'll see in a menu six weeks from now with no memory of this conversation. "Active customers (ordered in last 90 days)" beats "active_seg_v2".
71
45
 
72
46
  **Prudential calls — genuinely contextual, state your lean, let the user decide** (skip the ask in "Just go" mode — pick your lean, flag it):
73
47
 
@@ -76,7 +50,7 @@ The user already picked an autonomy mode (the router's Shared Contract asks the
76
50
  - "Let me add up revenue the same way everywhere, on this table" → a **measure** on the table.
77
51
  - "Revenue is an _official company number_ people pull onto dashboards" → a **metric** in a collection, with a default month-by-month view so it charts cleanly. Lean: make it a metric when it's a headline figure the org reuses across many questions/dashboards; keep it a measure when it's a table-local convenience.
78
52
  - **Where the metric lives.** Metrics sit in a collection (folder). Lean: put the org's blessed ones in the shared **Library** so they surface prominently; keep experimental ones in a working collection until trusted.
79
- - **Publish the official tables to the Library.** The clean, analysis-ready tables your definitions sit on are the org's official starting points — the **Library** is how you mark them as such. Tables published to the Library's **Data** section appear _first_ when anyone picks a data source, nudging people toward your curated tables instead of raw warehouse ones. Lean: publish the wide clean tables you built the semantic layer on; hold back raw or half-built ones. Setting up the Library is a one-time, harmless step (`mb library create` is idempotent). Surface which tables you'd publish and confirm. (Library is a Pro/Enterprise feature; only admins and data analysts can publish.)
53
+ - **Publish the official tables to the Library.** The clean, analysis-ready tables your definitions sit on are the org's official starting points — the **Library** is how you mark them as such. Tables published to the Library's **Data** section appear _first_ when anyone picks a data source, nudging people toward your curated tables instead of raw warehouse ones. Lean: publish the wide clean tables you built the semantic layer on; hold back raw or half-built ones. Surface which tables you'd publish and confirm. (Library is a Pro/Enterprise feature; only admins and data analysts can publish — mechanics in `core`.)
80
54
  - **Default time dimension for a metric.** A monthly default makes it chart nicely on a dashboard, but doesn't lock anyone out of other groupings. Lean: set a sensible default (usually month) for anything headline; leave it off for raw counts that aren't inherently time-series.
81
55
  - **How strict a segment is.** "Active" = last 30 vs 90 days is a real business call with no right answer from the data alone. Lean: surface the few reasonable thresholds with how many rows each catches, let the user pick.
82
56
 
@@ -84,8 +58,6 @@ Phrase a prudential call as a lean plus a nod:
84
58
 
85
59
  > "I'd save 'revenue' as a metric — Metabase's term for an official, reusable number — rather than a table-only measure, since people pull it onto dashboards a lot. Good?"
86
60
 
87
- ---
88
-
89
61
  ## The process
90
62
 
91
63
  ### Phase 0 — Understand what's reusable (quietly)
@@ -94,9 +66,9 @@ Don't narrate. One "Let me see what's here and how people are already slicing it
94
66
 
95
67
  1. **Confirm the analysis-ready tables exist.** List tables; find the wide, clean ones (a transform step's output). If the user is pointing you at raw normalized tables, say so plainly and suggest building the clean table first — don't build a hobbled semantic layer on raw data.
96
68
  2. **Find the questions people keep asking.** Search existing saved questions and dashboards (`mb search`, `mb card list`) for repeated filters and repeated calculations — the same "status = active" written eleven times, five hand-rolled versions of revenue. Those repeats _are_ the semantic layer waiting to be named. This is the highest-signal input; mine it before proposing anything.
97
- 3. **Learn the real meanings.** For every candidate segment ("active", "churned", "high-value"), find what the words map to in actual values — distinct values of a status column, the spread of an amount column. Never define on a guessed meaning (hard rule 1).
69
+ 3. **Learn the real meanings.** For every candidate segment ("active", "churned", "high-value"), find what the words map to in actual values — distinct values of a status column, the spread of an amount column. Pin every definition to real data (hard rule 1).
98
70
  4. **Graft onto what the org already tracks.** This is the part a model does worst and a human does best, so lean on the user: a new definition is far more useful when it lines up with the entities and language the organization _already_ uses. Before inventing "customer health score", ask whether there's already a notion of an active/at-risk customer in their world, and match it. Isolated definitions that don't connect to the existing model are low-value. Ask; don't infer the connection from column names.
99
- 5. **Check reach before promising.** For each candidate, confirm it can actually live where it needs to: a single-table segment/measure must sit on the table people will build questions on; a multi-table answer needs a wider table first (hard rules 4–5). Catch this now, not after building something that won't appear.
71
+ 5. **Check reach before promising.** For each candidate, confirm it can actually live where it needs to: a single-table segment/measure must sit on the table people will build questions on; a multi-table answer needs a wider table first (hard rule 4). Catch this now, not after building something that won't appear.
100
72
 
101
73
  ### Phase 1 — Propose the shared vocabulary (plain language)
102
74
 
@@ -120,16 +92,16 @@ Then surface what you're _not_ saving and why ("I left 'orders this week' alone
120
92
 
121
93
  ### Phase 2 — Iterate (cheap, nothing built yet)
122
94
 
123
- Adjust names, meanings, thresholds, and which-kind-of-thing until the user is happy. Re-confirm the final list in one short recap. If a definition turns out to need more than one table, say so plainly and point back to making the table wider — don't smuggle in a join.
95
+ Adjust names, meanings, thresholds, and which-kind-of-thing until the user is happy. Re-confirm the final list in one short recap. If a definition turns out to need more than one table, say so plainly and point back to making the table wider (hard rule 4) — don't smuggle in a join.
124
96
 
125
97
  ### Phase 3 — Build, verify quietly, then hard-stop
126
98
 
127
- Build each agreed definition. Mechanics (load `mbql` for the definition bodies):
99
+ Build each agreed definition. The verb mechanics (create/update flags, the `revision_message` audit-note rule on `update`, never delete-and-recreate) live in `core`; the definition bodies live in `mbql`:
128
100
 
129
- - **Segment** → `mb segment create`. Body: `name`, `table_id`, and a `definition` (a flat MBQL filter clause). Update later with `mb segment update <id>` — needs a `revision_message` (the audit note: _why_ it changed). Never delete-and-recreate.
130
- - **Measure** → `mb measure create`. Body: `name`, `table_id`, and a `definition` holding **exactly one** aggregation. Same `revision_message` rule on update.
131
- - **Metric** → `mb card create` with the metric shape (`type: "metric"`) — it lives in a **collection**, carries a `dataset_query` (the aggregation) and an optional default time dimension. Put org-blessed ones in the Library collection.
132
- - **Publish the official tables** → `mb library create` (idempotent — provisions the Library if it isn't set up yet), then `mb library publish --table-ids <ids>` to move the clean tables your definitions sit on into the Library's **Data** section. It resolves the Data collection itself, so there's no collection id to hunt for. Published tables surface first in every data picker, so people start from your curated set, not raw warehouse tables. (Run `mb skills get core` for the `library` group; it needs `library` premium + admin/data-analyst.)
101
+ - **Segment** → `mb segment create`. A flat MBQL filter clause on a table.
102
+ - **Measure** → `mb measure create`. **Exactly one** aggregation on a table.
103
+ - **Metric** → `mb card create` with the metric shape (`type: "metric"`) — it lives in a **collection**, carries the aggregation plus an optional default time dimension. Put org-blessed ones in the Library collection.
104
+ - **Publish the official tables** → `mb library create` then `mb library publish` (mechanics in `core`) to move the clean tables your definitions sit on into the Library's **Data** section, so people start from your curated set, not raw warehouse tables.
133
105
 
134
106
  Then **verify what the user can't see**, before you hand back:
135
107
 
@@ -158,14 +130,12 @@ Then **stop. Hard gate — every mode, no exceptions.** Recap in plain language
158
130
 
159
131
  End on that plain-language map. It's what the user reads to trust the result — and it's what stops a wrong definition from quietly propagating into everything built next.
160
132
 
161
- ---
162
-
163
133
  ## A worked example (for your reference, not the user's)
164
134
 
165
135
  User: _"Everyone calculates 'active users' differently — can you make it official?"_
166
136
 
167
137
  - **Don't** create a segment from the phrase alone. **Find the real meaning first:** search existing questions — three people filter on "last seen in the last 30 days", two on "subscription status = active". That's the ambiguity to resolve. Ask: "I see two takes on 'active' — seen in the last 30 days, or has a live subscription. Which do you mean?" (hard rule 1).
168
- - They say "live subscription, and seen in the last 30 days." **Check reach:** both pieces of info must live on the one table people build questions on. If subscription status and last-seen sit on two different tables, a single segment can't span them (hard rule 4) — to the user: "those two facts live in different places right now, so I'll widen your Customers table to carry both first, then save the filter on it." Build the transform, then the segment on the wide table.
138
+ - They say "live subscription, and seen in the last 30 days." **Check reach:** both pieces of info must live on the one table people build questions on. If subscription status and last-seen sit on two different tables, a single segment can't span them (hard rule 4) — to the user: "those two facts live in different places right now, so I'll widen your Customers table to carry both first, then save the filter on it." Make the table wider first (the **build-clean-tables** stage, `references/building-clean-tables.md`; the `transform` skill has the mechanics), then the segment on the wide table.
169
139
  - Build it as a segment on the wide table. **Verify** the row count is plausible. **Recap** plainly and stop: "Saved **Active users** — live subscription and seen in the last 30 days — as a segment on your Customers table; it's in the Filter block there. Have a look before you build on it."
170
140
 
171
141
  The shape recurs: a word people use loosely → pin it to real values → check it can live where they'll use it → build → verify → hard-stop with a plain recap.
@@ -6,9 +6,9 @@ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
6
6
 
7
7
  # Documents
8
8
 
9
- A **document** is a Metabase rich-text page (a "report" / notebook) that mixes prose with embedded saved questions and links to other Metabase entities. The body is a **TipTap** JSON tree (TipTap is the editor; the wire format is ProseMirror JSON, and the server stores it under `content_type: "application/json+vnd.prose-mirror"`).
9
+ A **document** is a Metabase rich-text page (a "report" / notebook) that mixes prose with embedded saved questions and links to other Metabase entities. The body is a **TipTap** JSON tree (TipTap is the editor; the wire format is ProseMirror JSON, stored under `content_type: "application/json+vnd.prose-mirror"`).
10
10
 
11
- This skill covers authoring the body and driving the verbs. General flag conventions, body-input precedence, and output flags live in the `core` skill (`mb skills get core`).
11
+ This skill covers authoring the body and driving the verbs. Flag conventions, body-input precedence, `./.scratch`, and `mb uuid` live in `core` (`mb skills get core`).
12
12
 
13
13
  ## Command surface
14
14
 
@@ -20,19 +20,15 @@ mb document update <id> --file patch.json --profile <name> --json # PATCH sema
20
20
  mb document archive <id> --profile <name> --json # soft-delete (PUT archived:true)
21
21
  ```
22
22
 
23
- - `list` returns the standard envelope (`{data, returned, total}`). The compact item is `{id, name, collection_id, archived, creator_id, can_write}` — it does **not** include the (potentially huge) `document` body. Pull the body with `get --full`.
23
+ - `list` returns the standard envelope (`{data, returned, total}`). The compact item is `{id, name, collection_id, archived, creator_id, can_write}` and omits the (potentially huge) `document` body — pull the body with `get --full`.
24
24
  - `archive` is the only delete, mirroring `card` / `dashboard`. **Unarchive** with `mb document update <id> --body '{"archived":false}'`.
25
- - `update` is PATCH — send only the keys you want to change (`name`, `document`, `collection_id`, `collection_position`, `archived`). Replacing `document` replaces the **whole** body; there is no partial-node patch.
25
+ - `update` is PATCH — send only the keys you want to change, and only these are accepted: `name`, `document`, `collection_id`, `collection_position`, `archived`. Replacing `document` replaces the **whole** body; there is no partial-node patch.
26
26
 
27
27
  ## Node ids (`_id`)
28
28
 
29
- The editor anchors only these node types with an `_id` (a UUID): `paragraph`, `heading`, `codeBlock`, `orderedList`, `bulletList`, `blockquote`, `cardEmbed`, `supportingText`. **`create`/`update` require a non-empty `_id` on every node of those types** and reject a body missing any; other node types (`doc`, `text`, `listItem`, `resizeNode`, `flexContainer`, …) don't take an `_id` and are left alone. Mint the ids with the bundled `uuid` command — one per id-bearing node:
29
+ The editor anchors only these node types with an `_id` (a UUID): `paragraph`, `heading`, `codeBlock`, `orderedList`, `bulletList`, `blockquote`, `cardEmbed`, `supportingText`. **`create`/`update` require a non-empty `_id` on every node of those types** and reject a body missing any ("did not match expected schema"). Other node types (`doc`, `text`, `listItem`, `resizeNode`, `flexContainer`, …) take no `_id` and are left alone. Without them the editor backfills ids when the document opens, which makes a freshly-saved document show a spurious "unsaved changes" prompt.
30
30
 
31
- ```bash
32
- mb uuid --count 5 --json # → ["…", …] one UUID per id-bearing node
33
- ```
34
-
35
- Set each as that node's `attrs._id`. Without them the editor backfills ids when the document opens, which makes a freshly-saved document show a spurious "unsaved changes" prompt.
31
+ Mint the ids with `mb uuid --count <n> --json` (→ `["…", …]`), one per id-bearing node, and set each as that node's `attrs._id`.
36
32
 
37
33
  ## Body shape (create / update)
38
34
 
@@ -90,7 +86,7 @@ Every node is `{ "type": string, "attrs"?: object, "content"?: [nodes], "text"?:
90
86
 
91
87
  ## Embedding an existing card
92
88
 
93
- A document embedding an existing card (id 114) under a heading (only the id-bearing nodes carry `_id`):
89
+ Find the id with `mb card list --profile <name> --json` (or `mb search --models card "<text>"`), then reference it in a `cardEmbed`. A document embedding existing card 114 under a heading (only the id-bearing nodes carry `_id`):
94
90
 
95
91
  ```json
96
92
  {
@@ -111,8 +107,6 @@ A document embedding an existing card (id 114) under a heading (only the id-bear
111
107
  }
112
108
  ```
113
109
 
114
- To embed an existing card, find its id with `mb card list --profile <name> --json` (or `mb search --models card "<text>"`), then reference it in a `cardEmbed`.
115
-
116
110
  ## Creating brand-new cards inline with the document
117
111
 
118
112
  You can create cards atomically with the document instead of pre-creating them. Reference each new card by a **negative** id in its `cardEmbed.attrs.id`, then supply the card definitions in a top-level `cards` map keyed by the same negative ids. The server creates the real cards and rewrites the negative ids to the real positive ids in the stored body.
@@ -138,11 +132,11 @@ You can create cards atomically with the document instead of pre-creating them.
138
132
  }
139
133
  ```
140
134
 
141
- Each entry in `cards` needs at least `{name, dataset_query, display, visualization_settings}` (these are card definitions, not TipTap nodes, so they take no `_id`). Author the `dataset_query` with the `mbql` skill (`mb skills get mbql`) and the `visualization_settings` with the `viz` skill. For most edits, prefer embedding cards that already exist (a plain positive `id` in `cardEmbed`) — inline creation is for "build the report and its questions in one shot".
135
+ Each entry in `cards` needs at least `{name, dataset_query, display, visualization_settings}` (these are card definitions, not TipTap nodes, so they take no `_id`). Author the `dataset_query` with `mbql` (`mb skills get mbql`) and the `visualization_settings` with `visualization`. For most edits, prefer embedding cards that already exist (a plain positive `id` in `cardEmbed`) — inline creation is for "build the report and its questions in one shot".
142
136
 
143
137
  ## Iterating on a document
144
138
 
145
- `update` replaces the whole `document` body, so the safe loop is **read → edit → write**. A fetched body already carries `_id`s on its id-bearing nodes, so preserve them — only mint new ones for id-bearing nodes you add:
139
+ `update` replaces the whole `document` body, so the safe loop is **read → edit → write**. A fetched body already carries `_id`s on its id-bearing nodes — preserve them, and only mint new ones for id-bearing nodes you add. Don't hand-merge a partial node tree into a live document; pull the current `document`, mutate the array, and PUT the whole thing back.
146
140
 
147
141
  ```bash
148
142
  mb document get <id> --full --profile <name> --json | jq '.document' > ./.scratch/body.json
@@ -151,12 +145,8 @@ jq -n --slurpfile d ./.scratch/body.json '{document: $d[0]}' > ./.scratch/patch.
151
145
  mb document update <id> --file ./.scratch/patch.json --profile <name> --json
152
146
  ```
153
147
 
154
- Don't hand-merge a partial node tree into a live document — pull the current `document`, mutate the array, and PUT the whole thing back. To rename without touching the body, patch only `name`: `mb document update <id> --body '{"name":"New title"}'`.
148
+ To rename without touching the body, patch only `name`: `mb document update <id> --body '{"name":"New title"}'`.
155
149
 
156
150
  ## Don't
157
151
 
158
- - Don't omit `_id` on an id-bearing node (`paragraph`, `heading`, `codeBlock`, `orderedList`, `bulletList`, `blockquote`, `cardEmbed`, `supportingText`) — `create`/`update` reject the body ("did not match expected schema"). Mint ids with `mb uuid`.
159
- - Don't paste a whole `document get` response into `update` — `update` only accepts `name`, `document`, `collection_id`, `collection_position`, `archived`. Send the body under the `document` key, not the full record.
160
- - Don't put the full `document` body in `list` expectations — `list` is compact and omits it by design; use `get --full`.
161
152
  - Don't invent node types. Stick to the inventory above; unknown block types render as empty/broken in the editor even though the response schema is lenient.
162
- - Don't author a card's `dataset_query` or `visualization_settings` from this skill alone — load `mbql` and `viz`.
@@ -6,39 +6,11 @@ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
6
6
 
7
7
  # git-sync (representations ↔ instance)
8
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/*`.
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/*`. Only collections flagged for sync serialize; everything else is local-only.
10
10
 
11
- This skill covers the import/export workflow. The general flag conventions and auth setup live in the `core` skill (`mb skills get core`). To author content YAML by hand: 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.
11
+ This skill covers the import/export workflow. Flag conventions and auth setup live in `core` (`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 `visualization` skill — except the portable YAML uses **name-based** references (e.g. `[Sample Database, PUBLIC, ORDERS, TOTAL]`, and entity-ids for cross-entity FKs) where the API form uses numeric ids. For the on-disk folder layout, model new files on what the synced repo already contains.
12
12
 
13
- ## 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
13
+ ## Precondition: read state before mutating
42
14
 
43
15
  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
16
 
@@ -50,7 +22,7 @@ mb git-sync dirty --profile <n> --json # → list the dirty obje
50
22
  mb git-sync current-task --profile <n> --json # → in-flight task (or idle)
51
23
  ```
52
24
 
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".
25
+ **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 transforms, prefer `transform update <id>` over delete + create (see the `transform` skill).
54
26
 
55
27
  ## Import (remote → instance)
56
28
 
@@ -71,8 +43,8 @@ Pulls the configured branch and applies it to the instance. Polls until the task
71
43
 
72
44
  Workflow:
73
45
 
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.
46
+ 1. Read state (above) — confirm `dirty: false` (or `--force` is intended).
47
+ 2. Confirm `has-remote-changes` reports there's actually something to import.
76
48
  3. `git-sync import --branch <branch>` — runs to terminal status by default.
77
49
 
78
50
  ## Export (instance → remote)
@@ -93,7 +65,7 @@ Pushes Metabase-side changes back to the configured remote. `-m` is the commit m
93
65
  Workflow:
94
66
 
95
67
  1. **Branch guard** (below) — confirm the instance isn't tracking `main`/`master`, or that the user has explicitly accepted exporting to it.
96
- 2. `git-sync is-dirty` — confirm there's something to export.
68
+ 2. Read state (above) — confirm `is-dirty` reports there's something to export.
97
69
  3. `git-sync export -m "..."` — pushes and polls.
98
70
  4. (Optional) `git-sync status` — verify `dirty: false` after.
99
71
 
@@ -131,6 +103,34 @@ mb git-sync cancel-task --profile <n> # cancel the in-flight task
131
103
 
132
104
  Use `wait` after `import --no-wait` / `export --no-wait`. Use `cancel-task` if a git-sync task hangs and you want to abandon it.
133
105
 
106
+ ## Adding / removing a directory (collection) to sync
107
+
108
+ 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`):
109
+
110
+ ```bash
111
+ mb git-sync add-collection <collection-id> --profile <n> --json
112
+ mb git-sync remove-collection <collection-id> --profile <n> --json
113
+ ```
114
+
115
+ `<collection-id>` is a **positive integer** (the bulk endpoint's schema is `pos-int? → boolean`; nano-id / `root` / `trash` refs are not supported). Get the id from `collection list` (see `core`).
116
+
117
+ 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.
118
+
119
+ **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.
120
+
121
+ **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:
122
+
123
+ ```bash
124
+ mb setting set remote-sync-type '"read-write"' --profile <n>
125
+ ```
126
+
127
+ (`setting set` parses the value as strict JSON — mind the inner double quotes; see `core`.) 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.
128
+
129
+ **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:
130
+
131
+ - `mb git-sync is-dirty --profile <n> --json` after editing a card in the now-synced collection — a `true` reading proves it's tracked.
132
+ - The Metabase Admin UI's Remote Sync page renders the per-collection toggles.
133
+
134
134
  ## Don't (git-sync-specific)
135
135
 
136
136
  - 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.
@@ -138,4 +138,4 @@ Use `wait` after `import --no-wait` / `export --no-wait`. Use `cancel-task` if a
138
138
  - 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.
139
139
  - Don't omit `-m` on `export` if the user wants a meaningful commit message — the default server-generated message is generic.
140
140
  - Don't `git-sync export` to `main`/`master` without explicit user confirmation — sync work is conventionally on a feature branch. See "Branch guard" above.
141
- - 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.
141
+ - 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>` (above), and remember the toggle cascades to descendants.
@@ -1,6 +1,6 @@
1
1
  ---
2
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 numeric-id stages), the options-object-always-second clause rule, when lib/uuid is needed (optional - only to reference a clause), the print-schema/dry-run/run loop, where MBQL 5 is consumed (mb query, card dataset_query, transform source.query, measure/segment definition), the flat-vs-legacy-envelope footgun, joins and FK traversal, multi-stage pipelines, naming aggregation columns. Load when 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", "join two tables", "month-over-month", or any `--dry-run` / `mb query` work.
3
+ description: Author and debug MBQL 5 query bodies for the `mb` CLI — the only hand-authorable query format. Covers the JSON shape (flat numeric-id stages, options-object-second clauses, optional lib/uuid), joins and FK traversal, multi-stage pipelines, aggregation naming, the flat-vs-legacy-envelope footgun, and the print-schema → dry-run → run validation loop. Use when writing or fixing any query body — `mb query`, a card's `dataset_query`, a transform's `source.query`, or a segment/measure `definition` — or when `--dry-run`/run reports validation errors. Triggers — "write an MBQL query", "the dataset_query is wrong", "aggregate and group by", "join two tables", "month-over-month".
4
4
  allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
5
5
  ---
6
6
 
@@ -10,7 +10,7 @@ MBQL 5 is the **only query format you can author by hand** with confidence — i
10
10
 
11
11
  Prefer MBQL over native SQL: portable across warehouse engines and pre-flight-validated. Try it first; fall back to native SQL when MBQL can't express what you need, or when an MBQL body keeps failing server-side and you can't resolve it.
12
12
 
13
- General flag conventions, body-input precedence, and output flags live in the `core` skill (`mb skills get core`).
13
+ General flag conventions, body-input precedence, output flags, `./.scratch`, and `mb uuid` mechanics live in `core` (`mb skills get core`).
14
14
 
15
15
  ## The shape
16
16
 
@@ -64,11 +64,7 @@ Set an explicit `lib/uuid` only when you must **reference a clause from elsewher
64
64
 
65
65
  (`AGG_UUID` is both the aggregation's own `lib/uuid` and the string the ref points at — one value, by string equality. Every other clause omits its UUID. Expression refs work the same way but key off the expression's `lib/expression-name` string, so expressions rarely need an explicit `lib/uuid`.)
66
66
 
67
- When you do need one, **always mint it with `mb uuid` — never write, guess, or copy a UUID yourself.** A hand-authored value is rejected pre-flight as not-a-v4 (`"a1"`, `"uuid-1"`, `"agg-uuid-001"` → `must be a UUID v4 (RFC 4122) — run \`mb uuid\``), or if it looks valid risks colliding with another clause. Only `mb uuid`gives genuine, unique v4s — mint just the few you reference (also covers native template-tag ids and any other`format: "uuid"` slot):
68
-
69
- ```bash
70
- mb uuid --count 2 --json # mint only the clauses you actually reference
71
- ```
67
+ When you do need one, **always mint it with `mb uuid` — never write, guess, or copy a UUID yourself.** A hand-authored value is rejected pre-flight as not-a-v4 (`"a1"`, `"uuid-1"`, `"agg-uuid-001"` → ``must be a UUID v4 (RFC 4122) — run `mb uuid` ``), or if it looks valid risks colliding with another clause. Mint just the few you reference (`mb uuid --count 2 --json`; this also covers native template-tag ids and any other `format: "uuid"` slot).
72
68
 
73
69
  ## Authoring loop: print-schema → dry-run → run
74
70
 
@@ -213,11 +209,3 @@ mb skills path mbql # → the skill dir; then Read references/operator
213
209
  ```
214
210
 
215
211
  `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.
216
-
217
- ## Don't
218
-
219
- - Don't mint a `lib/uuid` for every clause — they're optional; omit them and the server fills them in. Mint (with `mb uuid`) only the clause you need to reference; never invent, hard-code, or copy a UUID (duplicates are rejected server-side).
220
- - Keep the options object in slot 1 of every clause — `[op, {options}, ...args]`, id last (`["field", {}, 1779]`). The legacy `["field", id, opts]` order (id second) is rejected pre-flight.
221
- - Don't wrap an MBQL 5 body in `{type:"query", query:…}` — `dataset_query` / `source.query` / `definition` is the flat `mbql/query`.
222
- - Don't author MBQL 4 by hand — build it in the UI and pull it with `… get <id> --full --json`.
223
- - Don't skip the `--dry-run` loop on a non-trivial query — it's free and exact.