@metabase/cli 0.1.10 → 0.1.12

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 (159) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +9 -6
  3. package/dist/add-collection-BPtBMh8Y.mjs +10 -0
  4. package/dist/{add-collection-DUqTrC5T.mjs → add-collection-DEME4IOy.mjs} +4 -4
  5. package/dist/{archive-_GMNY8wH.mjs → archive-BFk-oupo.mjs} +3 -3
  6. package/dist/{archive-44EWiXud.mjs → archive-BNioNJtG.mjs} +3 -3
  7. package/dist/{archive-BZfpjMir.mjs → archive-BtDvBsr8.mjs} +3 -3
  8. package/dist/{archive-0krZxAXq.mjs → archive-CMeTr8jv.mjs} +3 -3
  9. package/dist/{archive-BEIyIsin.mjs → archive-DUlrUlok.mjs} +3 -3
  10. package/dist/{archive-DtE2H4A6.mjs → archive-Dc-FwNZm.mjs} +3 -3
  11. package/dist/{archive-B59Y7ajB.mjs → archive-MTTQv3wR.mjs} +3 -3
  12. package/dist/auth-CjFnuPe9.mjs +19 -0
  13. package/dist/{body-BdRyuvU4.mjs → body-89w3r7In.mjs} +2 -2
  14. package/dist/{branches-Jpv-FNds.mjs → branches-DuVKH8Ye.mjs} +4 -4
  15. package/dist/{cancel-ChC4lFd4.mjs → cancel-BEMQKVuR.mjs} +3 -3
  16. package/dist/{cancel-task-DyhIkNaL.mjs → cancel-task-D2mSPFXO.mjs} +4 -4
  17. package/dist/card-DoDpwR4t.mjs +20 -0
  18. package/dist/{cards-O-nKkQKP.mjs → cards-L9jksUMD.mjs} +3 -3
  19. package/dist/cli.mjs +23 -23
  20. package/dist/collection-CF-1RIPY.mjs +20 -0
  21. package/dist/{collection-namespace-7724zUMx.mjs → collection-namespace-w26SavJH.mjs} +1 -1
  22. package/dist/{create-D45uFXlo.mjs → create-8hMjPz7j.mjs} +4 -4
  23. package/dist/{create-B4f4Pldw.mjs → create-Bu9Jayck.mjs} +5 -5
  24. package/dist/{create-BxRsXQrm.mjs → create-C3b3MoIN.mjs} +4 -4
  25. package/dist/{create-DeZ2x2Db.mjs → create-Cxg_h8Kf.mjs} +4 -4
  26. package/dist/{create-DS52EhPd.mjs → create-D6QqOX6u.mjs} +4 -4
  27. package/dist/{create-BbF9zVFP.mjs → create-DCYc032c.mjs} +5 -5
  28. package/dist/{create-BnFHcnlL.mjs → create-Dew3rKsR.mjs} +4 -4
  29. package/dist/{create-CvYKJOcE.mjs → create-DoeqRVmx.mjs} +4 -4
  30. package/dist/{create-branch-KOWUIE72.mjs → create-branch-C5P2y8U5.mjs} +4 -4
  31. package/dist/{create-BuKx7kw6.mjs → create-zEq4ZJAT.mjs} +4 -4
  32. package/dist/{current-task-ClcWPPMc.mjs → current-task-uD4QO5bM.mjs} +4 -4
  33. package/dist/dashboard-C5zTX345.mjs +21 -0
  34. package/dist/db-4y5Z1CQw.mjs +22 -0
  35. package/dist/{delete-D68oS73R.mjs → delete-Bftif2R3.mjs} +3 -3
  36. package/dist/{delete-CX2VUA5R.mjs → delete-DGaNAFdQ.mjs} +3 -3
  37. package/dist/{delete-table-DLvL9mDA.mjs → delete-table-C0qxP6FE.mjs} +3 -3
  38. package/dist/{dirty-1OrXpc7E.mjs → dirty-CZZLpWt1.mjs} +4 -4
  39. package/dist/document-97l9Non_.mjs +19 -0
  40. package/dist/{eid-CzLhHZMW.mjs → eid-C8xTmR8e.mjs} +4 -4
  41. package/dist/{error-BWXBhqLW.mjs → error-BGH4gxGN.mjs} +2 -1
  42. package/dist/{export-B5z8w-xo.mjs → export-DWdjnBKo.mjs} +6 -6
  43. package/dist/field-BwUTrelF.mjs +18 -0
  44. package/dist/{fields-sC7pzmPX.mjs → fields-Cq08PQec.mjs} +3 -3
  45. package/dist/{get-CHb6J908.mjs → get-7IhxPyE_.mjs} +2 -2
  46. package/dist/{get-BYw3xS0X.mjs → get-8nAjRL2H.mjs} +3 -3
  47. package/dist/{get-ByZ4HR2T.mjs → get-B2vGW7Aw.mjs} +3 -3
  48. package/dist/{get-BC60bhel.mjs → get-B_BJ10pV.mjs} +3 -3
  49. package/dist/{get-DLwb_gUh.mjs → get-BaKKF50e.mjs} +3 -3
  50. package/dist/{get-Dl62Fy6Y.mjs → get-Bbij6RjC.mjs} +3 -3
  51. package/dist/{get-reSMTfQi.mjs → get-Bcr_5fAb.mjs} +2 -2
  52. package/dist/{get-lcX52Skc.mjs → get-Cr8oz_jr.mjs} +3 -3
  53. package/dist/{get-DTHLETau.mjs → get-DHPN1716.mjs} +3 -3
  54. package/dist/{get-C6n86-dS.mjs → get-DtKv-gl3.mjs} +3 -3
  55. package/dist/{get-ZesERdyk.mjs → get-DzLAmSKd.mjs} +2 -2
  56. package/dist/{get-Bo1FGyFs.mjs → get-Rqz7UHv1.mjs} +3 -3
  57. package/dist/{get-4GEDd9YN.mjs → get-f5ENeUNw.mjs} +3 -3
  58. package/dist/{get-run-CpCbHJad.mjs → get-run-DhbHXaZE.mjs} +3 -3
  59. package/dist/{get-Fn9WkNhS.mjs → get-ug26gcOI.mjs} +3 -3
  60. package/dist/git-sync-JIMEYW-E.mjs +28 -0
  61. package/dist/{has-remote-changes-CPz_-uxd.mjs → has-remote-changes-CTLh3WjZ.mjs} +4 -4
  62. package/dist/{import-DaWgprK6.mjs → import-XakIQfRp.mjs} +6 -6
  63. package/dist/{input-xewHccej.mjs → input-BZwm4yby.mjs} +25 -3
  64. package/dist/is-dirty-4gf8ujws.mjs +9 -0
  65. package/dist/{is-dirty-DZlI7lQx.mjs → is-dirty-DkOOyjsZ.mjs} +3 -3
  66. package/dist/{items-hgbRYsYD.mjs → items-DC985V8K.mjs} +3 -3
  67. package/dist/{list-BgHESP7b.mjs → list-B83-hnq5.mjs} +2 -2
  68. package/dist/{list-BGerkRHH.mjs → list-BtNboYpZ.mjs} +3 -3
  69. package/dist/{list-BWv5y307.mjs → list-CEbMS6g3.mjs} +2 -2
  70. package/dist/{list-CKeVS1IZ.mjs → list-CIEI8XK_.mjs} +2 -2
  71. package/dist/{list-Cd2nOCAx.mjs → list-Cnb9mQdd.mjs} +2 -2
  72. package/dist/{list-B9J3ujwn.mjs → list-CtAFlkLe.mjs} +2 -2
  73. package/dist/{list-36H-dvJZ.mjs → list-CyIgwqFB.mjs} +2 -2
  74. package/dist/{list-CGdOC9zX.mjs → list-Df4MhR2A.mjs} +2 -2
  75. package/dist/{list-DO8T-nmF.mjs → list-DfPwBQrZ.mjs} +2 -2
  76. package/dist/{list-DBlsRSpZ.mjs → list-OGkVAG6b.mjs} +2 -2
  77. package/dist/{list-DOVX3vCb.mjs → list-PYNFVJGM.mjs} +3 -3
  78. package/dist/{list-kath_2cX.mjs → list-STlBcPs8.mjs} +2 -2
  79. package/dist/{list-CCZnH2-Z.mjs → list-gy64AAWO.mjs} +2 -2
  80. package/dist/{list-D9EuxFHO.mjs → list-xfmTxHNu.mjs} +2 -2
  81. package/dist/{login-BzrAGJfu.mjs → login-C0VFlX8y.mjs} +4 -4
  82. package/dist/{logout-CKBiltoS.mjs → logout-BUgUTFol.mjs} +2 -2
  83. package/dist/measure-BmcmxRQ3.mjs +19 -0
  84. package/dist/{metadata-BcGcUEVJ.mjs → metadata-B1EmV1l5.mjs} +3 -3
  85. package/dist/{metadata-BjOrKtnv.mjs → metadata-Dwkcsa1c.mjs} +3 -3
  86. package/dist/{parse-id--iVTCKSo.mjs → parse-id-sKNP6yu5.mjs} +1 -1
  87. package/dist/{path-LgGU6Bd0.mjs → path-BZJR7YLR.mjs} +2 -2
  88. package/dist/{poll-BucRFJT-.mjs → poll-Cyw-dQi4.mjs} +1 -1
  89. package/dist/{poll-task-DTzKB3T3.mjs → poll-task-BG0oVnfZ.mjs} +2 -2
  90. package/dist/{preflight-CzqVX0PP.mjs → preflight-BjR63JnX.mjs} +1 -1
  91. package/dist/{query-BVZkK6Qk.mjs → query-C2xqnz24.mjs} +3 -3
  92. package/dist/{query-DG_jygDF.mjs → query-Dk55Wp2p.mjs} +4 -4
  93. package/dist/{remove-collection-HdeAfLyi.mjs → remove-collection-DbVNWGyb.mjs} +6 -6
  94. package/dist/{rescan-values-hWubCruZ.mjs → rescan-values-CuLQ9UHP.mjs} +3 -3
  95. package/dist/{run-C1-lDmQF.mjs → run-PDN33QjN.mjs} +5 -5
  96. package/dist/{runs-Q6DYQyqj.mjs → runs-B9T-7lei.mjs} +3 -3
  97. package/dist/{runtime-colqvhLf.mjs → runtime-DgHh4T6t.mjs} +1 -1
  98. package/dist/{schema-tables-BEastV_8.mjs → schema-tables-DjmNwrlT.mjs} +3 -3
  99. package/dist/{schemas-CgawwI_k.mjs → schemas-CWL8gv1C.mjs} +3 -3
  100. package/dist/{search-BWo7xSPP.mjs → search-D7JzmvfR.mjs} +3 -3
  101. package/dist/segment-DrZNuxsM.mjs +19 -0
  102. package/dist/{set-7Nm2ZTb_.mjs → set-C0dCLnUA.mjs} +4 -4
  103. package/dist/{setting-DSGXJehQ.mjs → setting-Licy7ZSs.mjs} +3 -3
  104. package/dist/{setup-aJLGLrIT.mjs → setup-BL4fqxoU.mjs} +4 -4
  105. package/dist/{skills-Q2AFsYvc.mjs → skills-Cn8FMCxG.mjs} +3 -3
  106. package/dist/snippet-C5_4nPGj.mjs +19 -0
  107. package/dist/{stash-DPQ0c-Cd.mjs → stash-SGod4B8-.mjs} +6 -6
  108. package/dist/{status-CvKPrV5X.mjs → status-BCb5iCsQ.mjs} +5 -5
  109. package/dist/{status-CvAATvV0.mjs → status-CvUuZVvC.mjs} +2 -2
  110. package/dist/{summary-CeOnoOq2.mjs → summary-qaTPTLWs.mjs} +3 -3
  111. package/dist/{sync-schema-aOPBc3CY.mjs → sync-schema-CjjAYTbO.mjs} +5 -5
  112. package/dist/table-DK1B8Hzq.mjs +19 -0
  113. package/dist/transform-Dh5746zH.mjs +24 -0
  114. package/dist/transform-job-D0nRGPYN.mjs +19 -0
  115. package/dist/{tree-MOQOBeAP.mjs → tree-C0MFcweC.mjs} +2 -2
  116. package/dist/{update-BWyCK8QV.mjs → update-BImaz9vj.mjs} +6 -6
  117. package/dist/{update-B6mg3AZD.mjs → update-BTzsE8GU.mjs} +5 -5
  118. package/dist/{update-BRrnfG0q.mjs → update-BUoWwmQV.mjs} +5 -5
  119. package/dist/{update-CitS-QRN.mjs → update-BqsGGrzx.mjs} +6 -6
  120. package/dist/{update-57uxZWcR.mjs → update-CHUUY-IB.mjs} +5 -5
  121. package/dist/{update-Bj9s0ri8.mjs → update-CJASCsG4.mjs} +5 -5
  122. package/dist/{update-DfNKr_vS.mjs → update-DCR0wFNl.mjs} +5 -5
  123. package/dist/{update-BkMWBzvk.mjs → update-Dty6LvU2.mjs} +5 -5
  124. package/dist/{update-dashcard-D_-ura3Y.mjs → update-dashcard-BkJaVVvf.mjs} +5 -5
  125. package/dist/{update-BD9xkglP.mjs → update-qoP7hZTg.mjs} +5 -5
  126. package/dist/{update-Dri4Zg2H.mjs → update-vk_M0eOR.mjs} +5 -5
  127. package/dist/{upgrade-CFkZ4USY.mjs → upgrade-TdaiAYbH.mjs} +2 -2
  128. package/dist/{uuid-DpinhSxA.mjs → uuid-BeUz8VpP.mjs} +2 -2
  129. package/dist/{values-BSS4DRxk.mjs → values-DR-3HcK6.mjs} +3 -3
  130. package/dist/{verify-B_A7v8TY.mjs → verify-D2uBlvmt.mjs} +1 -1
  131. package/dist/{wait-D3iSnjMM.mjs → wait-FAnqO-LT.mjs} +5 -5
  132. package/dist/{wait-flags-_LnHOeBA.mjs → wait-flags-B5BI_xob.mjs} +2 -2
  133. package/package.json +2 -1
  134. package/skill-data/core/SKILL.md +22 -23
  135. package/skill-data/data-analysis/SKILL.md +65 -0
  136. package/skill-data/data-transformation/SKILL.md +200 -0
  137. package/skill-data/document/SKILL.md +4 -4
  138. package/skill-data/mbql/SKILL.md +20 -20
  139. package/skill-data/robot-data-engineer/SKILL.md +142 -0
  140. package/skill-data/semantic-layer/SKILL.md +166 -0
  141. package/skill-data/transform/SKILL.md +46 -48
  142. package/skill-data/visualization/SKILL.md +5 -3
  143. package/skills/metabase-cli/SKILL.md +6 -0
  144. package/dist/add-collection-D9wXgmRj.mjs +0 -10
  145. package/dist/auth-cFC5m69m.mjs +0 -19
  146. package/dist/card-ClvGX6dQ.mjs +0 -20
  147. package/dist/collection-DjvowSJC.mjs +0 -20
  148. package/dist/dashboard-BFeURTOw.mjs +0 -21
  149. package/dist/db-CSH1kwQr.mjs +0 -22
  150. package/dist/document-KdT_Xj6r.mjs +0 -19
  151. package/dist/field-CTFnZI8G.mjs +0 -18
  152. package/dist/git-sync-C2vib8rx.mjs +0 -28
  153. package/dist/is-dirty-Bb0Rtj7x.mjs +0 -9
  154. package/dist/measure-Dw1QpRZa.mjs +0 -19
  155. package/dist/segment-DMuYvFjg.mjs +0 -19
  156. package/dist/snippet-Df2TrP7-.mjs +0 -19
  157. package/dist/table-pK4OkVtL.mjs +0 -19
  158. package/dist/transform-D60veFH8.mjs +0 -24
  159. package/dist/transform-job-DXt5LsrY.mjs +0 -19
@@ -0,0 +1,166 @@
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
+ ---
6
+
7
+ # Semantic Layer
8
+
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.
10
+
11
+ 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
+
13
+ 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
+
15
+ - **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>.)
17
+ - **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
+
19
+ 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
+
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.**
22
+
23
+ You drive everything through the `mb` CLI. Load the CLI skills you'll need:
24
+
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
+ ```
29
+
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:
47
+
48
+ | Mode | What you do |
49
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------- |
50
+ | **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. |
52
+ | **Just go** | Build the whole set, surface judgment calls as "here's what I picked and why — say the word to change any." |
53
+
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
+ ---
60
+
61
+ ## Two kinds of decisions
62
+
63
+ **Hard rules — absolutes, never ask:**
64
+
65
+ 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.
67
+ 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".
71
+
72
+ **Prudential calls — genuinely contextual, state your lean, let the user decide** (skip the ask in "Just go" mode — pick your lean, flag it):
73
+
74
+ - **Which kind of thing is it?** Same wish, three possible homes:
75
+ - "Let me filter to just the active ones" → a **segment** (saved filter).
76
+ - "Let me add up revenue the same way everywhere, on this table" → a **measure** on the table.
77
+ - "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
+ - **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
+ - **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.
80
+ - **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.
81
+
82
+ Phrase a prudential call as a lean plus a nod:
83
+
84
+ > "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?"
85
+
86
+ ---
87
+
88
+ ## The process
89
+
90
+ ### Phase 0 — Understand what's reusable (quietly)
91
+
92
+ Don't narrate. One "Let me see what's here and how people are already slicing it" is plenty. Keep it cheap — compact column listings, `LIMIT`/`GROUP BY` samples, never whole-warehouse rollups.
93
+
94
+ 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.
95
+ 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.
96
+ 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).
97
+ 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.
98
+ 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.
99
+
100
+ ### Phase 1 — Propose the shared vocabulary (plain language)
101
+
102
+ Show, in plain terms, the definitions worth saving — lead with what each _does for the user_, and name the Metabase feature so they learn it:
103
+
104
+ **Segments — saved filters** (so people pull up the same set with one click):
105
+
106
+ > • **Active customers** — ordered in the last 90 days. ~2,400 of your 6,000 customers.
107
+ > • **Big orders** — over $100. About 1 in 5 orders.
108
+
109
+ **Measures — saved calculations** (so everyone adds it up the same way):
110
+
111
+ > • **Net revenue** — total paid, minus refunds.
112
+ > • **Average order value** — net revenue per order.
113
+
114
+ **Metrics — official numbers** (the headline figures, for dashboards):
115
+
116
+ > • **Monthly recurring revenue** — I'd save this as a metric with a month-by-month default, since it's a dashboard headline. Good?
117
+
118
+ Then surface what you're _not_ saving and why ("I left 'orders this week' alone — it's a one-off, not something you'd reuse"). Ask your prudential questions — one at a time, lean-plus-nod. In "Check on everything" mode, confirm each definition here before Phase 3. In "Balanced", ask only the judgment calls. In "Just go", state your picks and move on.
119
+
120
+ ### Phase 2 — Iterate (cheap, nothing built yet)
121
+
122
+ 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.
123
+
124
+ ### Phase 3 — Build, verify quietly, then hard-stop
125
+
126
+ Build each agreed definition. Mechanics (load `mbql` for the definition bodies):
127
+
128
+ - **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.
129
+ - **Measure** → `mb measure create`. Body: `name`, `table_id`, and a `definition` holding **exactly one** aggregation. Same `revision_message` rule on update.
130
+ - **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.
131
+
132
+ Then **verify what the user can't see**, before you hand back:
133
+
134
+ - Each segment actually narrows the rows you expect (`mb query` / preview the count — does "active customers" really return ~2,400?).
135
+ - Each measure and metric returns a sane number, not null or an error.
136
+ - Each definition shows up **where the user will look for it** — on a question built on the right table. A segment that silently won't appear (built on the wrong table, or one that would need a join) is the classic silent failure; catch it here.
137
+
138
+ Then **stop. Hard gate — every mode, no exceptions.** Recap in plain language and hand the user something to open and eyeball:
139
+
140
+ > Done. Here's the shared set you can now reuse:
141
+ >
142
+ > **Segments** (saved filters — in the **Filter** block on the Customers and Orders tables):
143
+ > • **Active customers** — ordered in the last 90 days
144
+ > • **Big orders** — over $100
145
+ >
146
+ > **Measures** (saved calculations — in the **Summarize** block):
147
+ > • **Net revenue** • **Average order value**
148
+ >
149
+ > **Metric** (in your **Library**, charts by month):
150
+ > • **Monthly recurring revenue**
151
+ >
152
+ > Open any of those tables' Filter or Summarize block in Metabase to see them in place and try one — give it a look before you start building dashboards on top.
153
+
154
+ 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.
155
+
156
+ ---
157
+
158
+ ## A worked example (for your reference, not the user's)
159
+
160
+ User: _"Everyone calculates 'active users' differently — can you make it official?"_
161
+
162
+ - **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).
163
+ - 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.
164
+ - 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."
165
+
166
+ 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.
@@ -8,7 +8,7 @@ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
8
8
 
9
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
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`).
11
+ Flag conventions, body-input precedence, and output flags live in the `core` skill (`mb skills get core`). Deciding _which_ transforms to build — modeling a whole raw database into a set of clean, analysis-ready tables — is the `data-transformation` skill (`mb skills get data-transformation`).
12
12
 
13
13
  ## Body shape
14
14
 
@@ -17,37 +17,32 @@ A transform has two halves:
17
17
  - `source` — the query to run (`type: "query"`, with `query.type` of `native` or `mbql`).
18
18
  - `target` — the warehouse destination (`type: "table"`, with `database`, `schema`, `name`).
19
19
 
20
- Native SQL is the simplest source and the easiest to author by hand (see "Create + run" below). MBQL is what the Metabase UI emits and is more verbose; pull a sample with `mb transform get <id> --full --json` if you need its shape.
20
+ Native SQL is the simplest source and the easiest to author by hand. MBQL is what the Metabase UI emits and is more verbose; pull a sample with `mb transform get <id> --full --json` if you need its shape.
21
21
 
22
22
  For an **MBQL 5** `source.query` (`lib/type: "mbql/query"`), the body shape, the "options object is always second" clause rule, UUID minting, aggregation/order-by refs, naming aggregation output columns, and the `--print-schema` → `--dry-run` validation loop are all in the `mbql` skill — **`mb skills get mbql`**. The MBQL-5 pre-flight on `transform create`/`update` is documented there too (legacy MBQL 4 and native sources skip it). For a transform target, naming your aggregation output columns matters more than usual — a bare `count` / `avg_2` becomes the warehouse column name; see the `mbql` skill's "Naming aggregation output columns".
23
23
 
24
24
  ## Create + run (native SQL)
25
25
 
26
26
  ```bash
27
- cat > /tmp/transform.json <<'EOF'
28
- {
29
- "name": "user_counts_by_signup_year",
30
- "description": "Sample transform: counts users by year of signup",
31
- "source": {
32
- "type": "query",
33
- "query": {
34
- "type": "native",
35
- "database": <db-id>,
36
- "native": {
37
- "query": "SELECT date_trunc('year', created_at)::date AS signup_year, COUNT(*)::int AS user_count FROM public.users GROUP BY 1 ORDER BY 1"
38
- }
39
- }
40
- },
41
- "target": {
42
- "type": "table",
43
- "database": <db-id>,
44
- "schema": "public",
45
- "name": "user_counts_by_signup_year"
46
- }
47
- }
48
- EOF
49
-
50
- TRANSFORM_ID=$(mb transform create --file /tmp/transform.json --profile <name> --json | jq -r '.id')
27
+ # Author the SQL formatted — it's what `mb transform get` and the Metabase editor show.
28
+ cat > ./.scratch/user_counts_by_signup_year.sql <<'SQL'
29
+ SELECT
30
+ date_trunc('year', created_at)::date AS signup_year,
31
+ COUNT(*)::int AS user_count
32
+ FROM public.users
33
+ GROUP BY 1
34
+ ORDER BY 1
35
+ SQL
36
+
37
+ # Embed it with jq --rawfile so the newlines survive as \n in valid JSON (don't hand-write the SQL as one line).
38
+ jq -n --rawfile q ./.scratch/user_counts_by_signup_year.sql \
39
+ '{ name: "user_counts_by_signup_year",
40
+ description: "Sample transform: counts users by year of signup",
41
+ source: { type: "query", query: { type: "native", database: <db-id>, native: { query: $q } } },
42
+ target: { type: "table", database: <db-id>, schema: "public", name: "user_counts_by_signup_year" } }' \
43
+ > ./.scratch/transform.json
44
+
45
+ TRANSFORM_ID=$(mb transform create --file ./.scratch/transform.json --profile <name> --json | jq -r '.id')
51
46
  mb transform run "$TRANSFORM_ID" --wait --profile <name> --json
52
47
  ```
53
48
 
@@ -57,8 +52,8 @@ Notes:
57
52
  - Target `schema` is the schema the result table is written into (e.g. `public`).
58
53
  - `--wait` on `transform run` polls until status is `succeeded` or `failed`. Without it you only get `{message: "Transform run started", run_id, final: null}` and have to poll yourself.
59
54
  - `--sync` implies `--wait`, then waits until the run's output table is registered — the run registers it itself, no `db sync-schema` needed — adding `target_table_id` to the envelope. Use it when you'll build MBQL on the output (see "Inspect").
60
- - The `--json` envelope is shape-stable: `{message, run_id, final}` (plus `target_table_id` under `--sync` — a number, or `null` if the table didn't register before the timeout). `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.
61
- - The heredoc with single-quoted `'EOF'` prevents shell from interpolating any `$vars` inside the SQL.
55
+ - The `--json` envelope is shape-stable: `{message, run_id, final}` (plus `target_table_id` under `--sync` — a number, or `null` if the table didn't register before the timeout). `final` is `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.
56
+ - **Keep the SQL formatted.** Author it multi-line in `./.scratch/<name>.sql` and embed with `jq --rawfile` (jq ≥1.6, which JSON-encodes the file so newlines become `\n`). The stored `native.query` is what `mb transform get` and the Metabase editor render — a single-line blob is valid JSON but unreadable when anyone opens the transform. Single-quote the heredoc delimiter (`<<'SQL'`) so the shell leaves `$vars` in the query alone (e.g. Postgres `$1`, `$$`).
62
57
  - `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.
63
58
  - 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.
64
59
  - **`collection_id` only accepts a collection in the `:transforms` namespace.** Transforms aren't filed next to cards and dashboards — passing a normal analytics collection id (the kind a dashboard lives in) fails create/update with `collection_id: A Transform can only go in Collections in the :transforms namespace.` Omit `collection_id` to leave the transform uncollected (the common case), or create one with `mb collection create --body '{"name":"…"}' --namespace transforms --json` and pass the returned `id`. Cards and dashboards you build **on top of** the transform's output table go in ordinary collections as usual — so "put the transform and its dashboard in collection X" generally means _X holds the dashboard + cards; the transform stays in the transforms namespace._
@@ -70,14 +65,14 @@ mb transform list --profile <name> --json
70
65
  mb transform get <id> --profile <name> --full --json # full transform incl. last run summary
71
66
  ```
72
67
 
73
- After a run the table physically exists in the warehouse, but Metabase addresses tables/columns by numeric id, so **MBQL and the UI can't reference a brand-new table until the instance syncs** (native SQL — a native `card` or `mb query` against `<schema>.<name>` — reads it immediately). Run and register in one step with `--sync`:
68
+ After a run the table physically exists in the warehouse, but Metabase addresses tables/columns by numeric id, so **MBQL and the UI can't reference a brand-new table until the instance syncs** (native SQL — a native `card` or `mb query` against `<schema>.<name>` — reads it immediately). Run and register in one step with `--sync`.
74
69
 
75
70
  ```bash
76
71
  TABLE_ID=$(mb transform run <id> --sync --profile <name> --json | jq -r '.target_table_id')
77
72
  mb table get "$TABLE_ID" --include fields --profile <name> --json # field ids for MBQL
78
73
  ```
79
74
 
80
- `--sync` runs the transform and polls until its output table is registered, returning the id as `target_table_id` — the run registers the table itself, so no `db sync-schema` is needed. On `target_table_id: null` (still syncing when the poll timed out; exit 0) re-poll `mb transform get <id> --full --json` until the `target_table_id` / `table` linkage lands.
75
+ On `target_table_id: null` (still syncing when the poll timed out; exit 0) re-poll `mb transform get <id> --full --json` until the `target_table_id` / `table` linkage lands.
81
76
 
82
77
  Columns and types are inferred from the result set; change the SELECT shape and the next run fails on a column mismatch — drop the table first (`transform delete-table <id>`). A changed shape also needs a re-run with `--sync` before MBQL sees the new/renamed columns.
83
78
 
@@ -104,14 +99,14 @@ Notes:
104
99
 
105
100
  ## Update body: send only writable keys, never round-trip the GET body
106
101
 
107
- `transform update <id>` is **PATCH semantics** — only send the fields you actually want to change. The endpoint accepts exactly these writable keys:
102
+ `transform update <id>` is **PATCH semantics** — only send the fields you want to change. The endpoint accepts exactly these writable keys:
108
103
 
109
104
  ```
110
105
  name, description, source, target, run_trigger,
111
106
  tag_ids, collection_id, owner_user_id, owner_email
112
107
  ```
113
108
 
114
- **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:
109
+ **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. Unknown top-level keys flow into `t2/update!` and produce a leaked H2 SQL error like:
115
110
 
116
111
  ```
117
112
  Column "TAGS" not found; SQL statement:
@@ -130,13 +125,15 @@ Right shape — patch only what changes:
130
125
  # Rename only:
131
126
  mb transform update <id> --body '{"name":"renamed"}' --profile <name> --json
132
127
 
133
- # Rewrite the SQL only:
134
- cat > /tmp/patch.json <<'EOF'
135
- { "source": { "type": "query", "query": { "type": "native",
136
- "database": <db-id>,
137
- "native": { "query": "SELECT … FROM public.orders" } } } }
138
- EOF
139
- mb transform update <id> --file /tmp/patch.json --profile <name> --json
128
+ # Rewrite the SQL only — author it formatted, embed with jq:
129
+ cat > ./.scratch/orders.sql <<'SQL'
130
+ SELECT …
131
+ FROM public.orders
132
+ SQL
133
+ jq -n --rawfile q ./.scratch/orders.sql \
134
+ '{ source: { type: "query", query: { type: "native", database: <db-id>, native: { query: $q } } } }' \
135
+ > ./.scratch/patch.json
136
+ mb transform update <id> --file ./.scratch/patch.json --profile <name> --json
140
137
 
141
138
  # Change tag membership (note: tag_ids, not tags):
142
139
  mb transform update <id> --body '{"tag_ids":[1,3]}' --profile <name> --json
@@ -148,12 +145,12 @@ If you really must round-trip, project to the writable subset:
148
145
  mb transform get <id> --full --profile <name> --json \
149
146
  | jq '{name, description, source, target, run_trigger, tag_ids, collection_id, owner_user_id, owner_email}
150
147
  | with_entries(select(.value != null))' \
151
- > /tmp/patch.json
148
+ > ./.scratch/patch.json
152
149
  ```
153
150
 
154
151
  ## Iterating on a failing transform
155
152
 
156
- 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:
153
+ 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:
157
154
 
158
155
  - `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`.
159
156
  - You don't have to chase `_2` suffixes minted when two YAMLs share a `name` on disk (see the `transform create` notes above).
@@ -163,17 +160,18 @@ Recipe:
163
160
 
164
161
  ```bash
165
162
  # 1. Try once
166
- ID=$(mb transform create --file /tmp/t.json --profile <n> --json | jq -r '.id')
163
+ ID=$(mb transform create --file ./.scratch/t.json --profile <n> --json | jq -r '.id')
167
164
  mb transform run "$ID" --wait --profile <n> --json # → failed
168
165
 
169
166
  # 2. Fix the body in place; PATCH only what changed.
170
167
  # Source-only patch — keeps name, target, tags untouched on the server.
171
- cat > /tmp/source-patch.json <<'EOF'
172
- { "source": { "type": "query", "query": { "type": "native",
173
- "database": <db-id>,
174
- "native": { "query": "<fixed SQL here>" } } } }
175
- EOF
176
- mb transform update "$ID" --file /tmp/source-patch.json --profile <n> --json
168
+ cat > ./.scratch/source.sql <<'SQL'
169
+ <fixed SQL, formatted>
170
+ SQL
171
+ jq -n --rawfile q ./.scratch/source.sql \
172
+ '{ source: { type: "query", query: { type: "native", database: <db-id>, native: { query: $q } } } }' \
173
+ > ./.scratch/source-patch.json
174
+ mb transform update "$ID" --file ./.scratch/source-patch.json --profile <n> --json
177
175
 
178
176
  # 3. Re-run
179
177
  mb transform run "$ID" --wait --profile <n> --json # → succeeded
@@ -6,6 +6,8 @@ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
6
6
 
7
7
  # Visualization: pick the chart, then set it
8
8
 
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.
10
+
9
11
  A card has two presentation fields alongside its `dataset_query`:
10
12
 
11
13
  - **`display`** — the chart type (`bar`, `line`, `pie`, `scalar`, `map`, `table`, …). One closed set; pick from the enum below.
@@ -13,7 +15,7 @@ A card has two presentation fields alongside its `dataset_query`:
13
15
 
14
16
  Nothing validates `visualization_settings` — there is no pre-flight to fail past. A `display` typo or a misnamed key is accepted by the API; the card just renders as a default table or drops the setting. So **the feedback loop is read-back, not pre-flight**: after `card create`/`update`, confirm with `mb card get <id> --full --json` (or open the card) that it rendered as intended.
15
17
 
16
- General flag conventions and body-input precedence live in the `core` skill (`mb skills get core`); the `dataset_query` itself is the `mbql` skill's job (`mb skills get mbql`). This skill is only about how the result is displayed.
18
+ Flag conventions and body-input precedence live in the `core` skill (`mb skills get core`); the `dataset_query` itself is the `mbql` skill's job (`mb skills get mbql`). This skill is only about how the result is displayed.
17
19
 
18
20
  Two steps: **(1) pick the `display` that fits the data**, then **(2) bind the data columns and set options**.
19
21
 
@@ -64,7 +66,7 @@ Closed `display` enum (card-level, non-hidden): `table`, `bar`, `line`, `area`,
64
66
 
65
67
  `graph.dimensions`, `graph.metrics`, `pie.dimension`, `pie.metric`, `scalar.field`, `funnel.metric`, `map.latitude_column`, `sankey.source`, … all take **output column-name strings** — the names the query _produces_, not field ids. A `count` aggregation outputs the column `count`; a breakout on a field outputs that field's name; a named aggregation outputs its `name`. These strings are **identical in the API form and the portable (git-sync) form** — no numeric-vs-name footgun here.
66
68
 
67
- So the names you put in `visualization_settings` come from the query's output, not from `mb field`/`mb table`. If you set `name` on an aggregation (see the `mbql` skill), use that same string here.
69
+ The names come from the query's output, not from `mb field`/`mb table`. If you set `name` on an aggregation (see the `mbql` skill), use that same string here.
68
70
 
69
71
  ## Minimum-viable settings per chart family (API form)
70
72
 
@@ -136,7 +138,7 @@ For anything beyond a single dimension + metric — combo charts, conditional fo
136
138
  mb card get <id> --full --json | jq '.visualization_settings'
137
139
  ```
138
140
 
139
- Paste that block into your `card create`/`update` body. The server produced it, so it's valid for that `display`. This beats guessing keys from memory, and it's token-cheap.
141
+ Paste that block into your `card create`/`update` body. The server produced it, so it's valid for that `display`.
140
142
 
141
143
  ## Full per-visualization key catalog
142
144
 
@@ -19,3 +19,9 @@ Before running any `mb` command, load the workflow content from the CLI:
19
19
  mb skills get core # auth, flag conventions, every command group
20
20
  mb skills list # everything available on the installed version
21
21
  ```
22
+
23
+ **Doing a whole job, not one command?** If the user wants an outcome — "make sense of my data", "build a data model", "go from raw data to a dashboard", "answer questions about my data", "be my data analyst", "set up analytics for X" — load the front-door router instead and let it drive:
24
+
25
+ ```bash
26
+ mb skills get robot-data-engineer
27
+ ```
@@ -1,10 +0,0 @@
1
- import "./command-augment-BH9qgQ5u.mjs";
2
- import "./error-BWXBhqLW.mjs";
3
- import "./runtime-colqvhLf.mjs";
4
- import "./capabilities-7L9GVMd_.mjs";
5
- import "./parse-id--iVTCKSo.mjs";
6
- import "./poll-BucRFJT-.mjs";
7
- import "./poll-task-DTzKB3T3.mjs";
8
- import { SyncSettingsUpdateResult, add_collection_default, setCollectionRemoteSynced, syncSettingsUpdateView } from "./add-collection-DUqTrC5T.mjs";
9
-
10
- export { add_collection_default as default };
@@ -1,19 +0,0 @@
1
- import { defineCommand } from "citty";
2
-
3
- //#region src/commands/auth/index.ts
4
- var auth_default = defineCommand({
5
- meta: {
6
- name: "auth",
7
- description: "Authenticate against a Metabase instance"
8
- },
9
- default: "login",
10
- subCommands: {
11
- login: () => import("./login-BzrAGJfu.mjs").then((m) => m.default),
12
- status: () => import("./status-CvAATvV0.mjs").then((m) => m.default),
13
- list: () => import("./list-BGerkRHH.mjs").then((m) => m.default),
14
- logout: () => import("./logout-CKBiltoS.mjs").then((m) => m.default)
15
- }
16
- });
17
-
18
- //#endregion
19
- export { auth_default as default };
@@ -1,20 +0,0 @@
1
- import { defineCommand } from "citty";
2
-
3
- //#region src/commands/card/index.ts
4
- var card_default = defineCommand({
5
- meta: {
6
- name: "card",
7
- description: "Manage Metabase cards (questions, models, metrics)"
8
- },
9
- subCommands: {
10
- list: () => import("./list-CGdOC9zX.mjs").then((mod) => mod.default),
11
- get: () => import("./get-BYw3xS0X.mjs").then((mod) => mod.default),
12
- query: () => import("./query-BVZkK6Qk.mjs").then((mod) => mod.default),
13
- create: () => import("./create-CvYKJOcE.mjs").then((mod) => mod.default),
14
- update: () => import("./update-BkMWBzvk.mjs").then((mod) => mod.default),
15
- archive: () => import("./archive-44EWiXud.mjs").then((mod) => mod.default)
16
- }
17
- });
18
-
19
- //#endregion
20
- export { card_default as default };
@@ -1,20 +0,0 @@
1
- import { defineCommand } from "citty";
2
-
3
- //#region src/commands/collection/index.ts
4
- var collection_default = defineCommand({
5
- meta: {
6
- name: "collection",
7
- description: "Manage Metabase collections"
8
- },
9
- subCommands: {
10
- list: () => import("./list-36H-dvJZ.mjs").then((mod) => mod.default),
11
- get: () => import("./get-reSMTfQi.mjs").then((mod) => mod.default),
12
- items: () => import("./items-hgbRYsYD.mjs").then((mod) => mod.default),
13
- tree: () => import("./tree-MOQOBeAP.mjs").then((mod) => mod.default),
14
- create: () => import("./create-DS52EhPd.mjs").then((mod) => mod.default),
15
- archive: () => import("./archive-BZfpjMir.mjs").then((mod) => mod.default)
16
- }
17
- });
18
-
19
- //#endregion
20
- export { collection_default as default };
@@ -1,21 +0,0 @@
1
- import { defineCommand } from "citty";
2
-
3
- //#region src/commands/dashboard/index.ts
4
- var dashboard_default = defineCommand({
5
- meta: {
6
- name: "dashboard",
7
- description: "Manage Metabase dashboards"
8
- },
9
- subCommands: {
10
- list: () => import("./list-DBlsRSpZ.mjs").then((mod) => mod.default),
11
- get: () => import("./get-ByZ4HR2T.mjs").then((mod) => mod.default),
12
- cards: () => import("./cards-O-nKkQKP.mjs").then((mod) => mod.default),
13
- create: () => import("./create-B4f4Pldw.mjs").then((mod) => mod.default),
14
- update: () => import("./update-CitS-QRN.mjs").then((mod) => mod.default),
15
- "update-dashcard": () => import("./update-dashcard-D_-ura3Y.mjs").then((mod) => mod.default),
16
- archive: () => import("./archive-0krZxAXq.mjs").then((mod) => mod.default)
17
- }
18
- });
19
-
20
- //#endregion
21
- export { dashboard_default as default };
@@ -1,22 +0,0 @@
1
- import { defineCommand } from "citty";
2
-
3
- //#region src/commands/db/index.ts
4
- var db_default = defineCommand({
5
- meta: {
6
- name: "db",
7
- description: "Inspect and sync Metabase databases",
8
- alias: "database"
9
- },
10
- subCommands: {
11
- list: () => import("./list-Cd2nOCAx.mjs").then((m) => m.default),
12
- get: () => import("./get-Fn9WkNhS.mjs").then((m) => m.default),
13
- metadata: () => import("./metadata-BjOrKtnv.mjs").then((m) => m.default),
14
- schemas: () => import("./schemas-CgawwI_k.mjs").then((m) => m.default),
15
- "schema-tables": () => import("./schema-tables-BEastV_8.mjs").then((m) => m.default),
16
- "sync-schema": () => import("./sync-schema-aOPBc3CY.mjs").then((m) => m.default),
17
- "rescan-values": () => import("./rescan-values-hWubCruZ.mjs").then((m) => m.default)
18
- }
19
- });
20
-
21
- //#endregion
22
- export { db_default as default };
@@ -1,19 +0,0 @@
1
- import { defineCommand } from "citty";
2
-
3
- //#region src/commands/document/index.ts
4
- var document_default = defineCommand({
5
- meta: {
6
- name: "document",
7
- description: "Manage Metabase documents"
8
- },
9
- subCommands: {
10
- list: () => import("./list-B9J3ujwn.mjs").then((mod) => mod.default),
11
- get: () => import("./get-BC60bhel.mjs").then((mod) => mod.default),
12
- create: () => import("./create-D45uFXlo.mjs").then((mod) => mod.default),
13
- update: () => import("./update-57uxZWcR.mjs").then((mod) => mod.default),
14
- archive: () => import("./archive-_GMNY8wH.mjs").then((mod) => mod.default)
15
- }
16
- });
17
-
18
- //#endregion
19
- export { document_default as default };
@@ -1,18 +0,0 @@
1
- import { defineCommand } from "citty";
2
-
3
- //#region src/commands/field/index.ts
4
- var field_default = defineCommand({
5
- meta: {
6
- name: "field",
7
- description: "Manage Metabase fields"
8
- },
9
- subCommands: {
10
- get: () => import("./get-Dl62Fy6Y.mjs").then((m) => m.default),
11
- values: () => import("./values-BSS4DRxk.mjs").then((m) => m.default),
12
- summary: () => import("./summary-CeOnoOq2.mjs").then((m) => m.default),
13
- update: () => import("./update-B6mg3AZD.mjs").then((m) => m.default)
14
- }
15
- });
16
-
17
- //#endregion
18
- export { field_default as default };
@@ -1,28 +0,0 @@
1
- import { defineCommand } from "citty";
2
-
3
- //#region src/commands/git-sync/index.ts
4
- var git_sync_default = defineCommand({
5
- meta: {
6
- name: "git-sync",
7
- description: "Sync Metabase content with a git remote"
8
- },
9
- subCommands: {
10
- status: () => import("./status-CvKPrV5X.mjs").then((mod) => mod.default),
11
- "is-dirty": () => import("./is-dirty-Bb0Rtj7x.mjs").then((mod) => mod.default),
12
- "has-remote-changes": () => import("./has-remote-changes-CPz_-uxd.mjs").then((mod) => mod.default),
13
- dirty: () => import("./dirty-1OrXpc7E.mjs").then((mod) => mod.default),
14
- "current-task": () => import("./current-task-ClcWPPMc.mjs").then((mod) => mod.default),
15
- "cancel-task": () => import("./cancel-task-DyhIkNaL.mjs").then((mod) => mod.default),
16
- wait: () => import("./wait-D3iSnjMM.mjs").then((mod) => mod.default),
17
- import: () => import("./import-DaWgprK6.mjs").then((mod) => mod.default),
18
- export: () => import("./export-B5z8w-xo.mjs").then((mod) => mod.default),
19
- stash: () => import("./stash-DPQ0c-Cd.mjs").then((mod) => mod.default),
20
- branches: () => import("./branches-Jpv-FNds.mjs").then((mod) => mod.default),
21
- "create-branch": () => import("./create-branch-KOWUIE72.mjs").then((mod) => mod.default),
22
- "add-collection": () => import("./add-collection-D9wXgmRj.mjs").then((mod) => mod.default),
23
- "remove-collection": () => import("./remove-collection-HdeAfLyi.mjs").then((mod) => mod.default)
24
- }
25
- });
26
-
27
- //#endregion
28
- export { git_sync_default as default };
@@ -1,9 +0,0 @@
1
- import "./command-augment-BH9qgQ5u.mjs";
2
- import "./error-BWXBhqLW.mjs";
3
- import "./runtime-colqvhLf.mjs";
4
- import "./capabilities-7L9GVMd_.mjs";
5
- import "./poll-BucRFJT-.mjs";
6
- import "./poll-task-DTzKB3T3.mjs";
7
- import { IsDirtyResult, is_dirty_default } from "./is-dirty-DZlI7lQx.mjs";
8
-
9
- export { is_dirty_default as default };