@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +9 -6
- package/dist/add-collection-BPtBMh8Y.mjs +10 -0
- package/dist/{add-collection-DUqTrC5T.mjs → add-collection-DEME4IOy.mjs} +4 -4
- package/dist/{archive-_GMNY8wH.mjs → archive-BFk-oupo.mjs} +3 -3
- package/dist/{archive-44EWiXud.mjs → archive-BNioNJtG.mjs} +3 -3
- package/dist/{archive-BZfpjMir.mjs → archive-BtDvBsr8.mjs} +3 -3
- package/dist/{archive-0krZxAXq.mjs → archive-CMeTr8jv.mjs} +3 -3
- package/dist/{archive-BEIyIsin.mjs → archive-DUlrUlok.mjs} +3 -3
- package/dist/{archive-DtE2H4A6.mjs → archive-Dc-FwNZm.mjs} +3 -3
- package/dist/{archive-B59Y7ajB.mjs → archive-MTTQv3wR.mjs} +3 -3
- package/dist/auth-CjFnuPe9.mjs +19 -0
- package/dist/{body-BdRyuvU4.mjs → body-89w3r7In.mjs} +2 -2
- package/dist/{branches-Jpv-FNds.mjs → branches-DuVKH8Ye.mjs} +4 -4
- package/dist/{cancel-ChC4lFd4.mjs → cancel-BEMQKVuR.mjs} +3 -3
- package/dist/{cancel-task-DyhIkNaL.mjs → cancel-task-D2mSPFXO.mjs} +4 -4
- package/dist/card-DoDpwR4t.mjs +20 -0
- package/dist/{cards-O-nKkQKP.mjs → cards-L9jksUMD.mjs} +3 -3
- package/dist/cli.mjs +23 -23
- package/dist/collection-CF-1RIPY.mjs +20 -0
- package/dist/{collection-namespace-7724zUMx.mjs → collection-namespace-w26SavJH.mjs} +1 -1
- package/dist/{create-D45uFXlo.mjs → create-8hMjPz7j.mjs} +4 -4
- package/dist/{create-B4f4Pldw.mjs → create-Bu9Jayck.mjs} +5 -5
- package/dist/{create-BxRsXQrm.mjs → create-C3b3MoIN.mjs} +4 -4
- package/dist/{create-DeZ2x2Db.mjs → create-Cxg_h8Kf.mjs} +4 -4
- package/dist/{create-DS52EhPd.mjs → create-D6QqOX6u.mjs} +4 -4
- package/dist/{create-BbF9zVFP.mjs → create-DCYc032c.mjs} +5 -5
- package/dist/{create-BnFHcnlL.mjs → create-Dew3rKsR.mjs} +4 -4
- package/dist/{create-CvYKJOcE.mjs → create-DoeqRVmx.mjs} +4 -4
- package/dist/{create-branch-KOWUIE72.mjs → create-branch-C5P2y8U5.mjs} +4 -4
- package/dist/{create-BuKx7kw6.mjs → create-zEq4ZJAT.mjs} +4 -4
- package/dist/{current-task-ClcWPPMc.mjs → current-task-uD4QO5bM.mjs} +4 -4
- package/dist/dashboard-C5zTX345.mjs +21 -0
- package/dist/db-4y5Z1CQw.mjs +22 -0
- package/dist/{delete-D68oS73R.mjs → delete-Bftif2R3.mjs} +3 -3
- package/dist/{delete-CX2VUA5R.mjs → delete-DGaNAFdQ.mjs} +3 -3
- package/dist/{delete-table-DLvL9mDA.mjs → delete-table-C0qxP6FE.mjs} +3 -3
- package/dist/{dirty-1OrXpc7E.mjs → dirty-CZZLpWt1.mjs} +4 -4
- package/dist/document-97l9Non_.mjs +19 -0
- package/dist/{eid-CzLhHZMW.mjs → eid-C8xTmR8e.mjs} +4 -4
- package/dist/{error-BWXBhqLW.mjs → error-BGH4gxGN.mjs} +2 -1
- package/dist/{export-B5z8w-xo.mjs → export-DWdjnBKo.mjs} +6 -6
- package/dist/field-BwUTrelF.mjs +18 -0
- package/dist/{fields-sC7pzmPX.mjs → fields-Cq08PQec.mjs} +3 -3
- package/dist/{get-CHb6J908.mjs → get-7IhxPyE_.mjs} +2 -2
- package/dist/{get-BYw3xS0X.mjs → get-8nAjRL2H.mjs} +3 -3
- package/dist/{get-ByZ4HR2T.mjs → get-B2vGW7Aw.mjs} +3 -3
- package/dist/{get-BC60bhel.mjs → get-B_BJ10pV.mjs} +3 -3
- package/dist/{get-DLwb_gUh.mjs → get-BaKKF50e.mjs} +3 -3
- package/dist/{get-Dl62Fy6Y.mjs → get-Bbij6RjC.mjs} +3 -3
- package/dist/{get-reSMTfQi.mjs → get-Bcr_5fAb.mjs} +2 -2
- package/dist/{get-lcX52Skc.mjs → get-Cr8oz_jr.mjs} +3 -3
- package/dist/{get-DTHLETau.mjs → get-DHPN1716.mjs} +3 -3
- package/dist/{get-C6n86-dS.mjs → get-DtKv-gl3.mjs} +3 -3
- package/dist/{get-ZesERdyk.mjs → get-DzLAmSKd.mjs} +2 -2
- package/dist/{get-Bo1FGyFs.mjs → get-Rqz7UHv1.mjs} +3 -3
- package/dist/{get-4GEDd9YN.mjs → get-f5ENeUNw.mjs} +3 -3
- package/dist/{get-run-CpCbHJad.mjs → get-run-DhbHXaZE.mjs} +3 -3
- package/dist/{get-Fn9WkNhS.mjs → get-ug26gcOI.mjs} +3 -3
- package/dist/git-sync-JIMEYW-E.mjs +28 -0
- package/dist/{has-remote-changes-CPz_-uxd.mjs → has-remote-changes-CTLh3WjZ.mjs} +4 -4
- package/dist/{import-DaWgprK6.mjs → import-XakIQfRp.mjs} +6 -6
- package/dist/{input-xewHccej.mjs → input-BZwm4yby.mjs} +25 -3
- package/dist/is-dirty-4gf8ujws.mjs +9 -0
- package/dist/{is-dirty-DZlI7lQx.mjs → is-dirty-DkOOyjsZ.mjs} +3 -3
- package/dist/{items-hgbRYsYD.mjs → items-DC985V8K.mjs} +3 -3
- package/dist/{list-BgHESP7b.mjs → list-B83-hnq5.mjs} +2 -2
- package/dist/{list-BGerkRHH.mjs → list-BtNboYpZ.mjs} +3 -3
- package/dist/{list-BWv5y307.mjs → list-CEbMS6g3.mjs} +2 -2
- package/dist/{list-CKeVS1IZ.mjs → list-CIEI8XK_.mjs} +2 -2
- package/dist/{list-Cd2nOCAx.mjs → list-Cnb9mQdd.mjs} +2 -2
- package/dist/{list-B9J3ujwn.mjs → list-CtAFlkLe.mjs} +2 -2
- package/dist/{list-36H-dvJZ.mjs → list-CyIgwqFB.mjs} +2 -2
- package/dist/{list-CGdOC9zX.mjs → list-Df4MhR2A.mjs} +2 -2
- package/dist/{list-DO8T-nmF.mjs → list-DfPwBQrZ.mjs} +2 -2
- package/dist/{list-DBlsRSpZ.mjs → list-OGkVAG6b.mjs} +2 -2
- package/dist/{list-DOVX3vCb.mjs → list-PYNFVJGM.mjs} +3 -3
- package/dist/{list-kath_2cX.mjs → list-STlBcPs8.mjs} +2 -2
- package/dist/{list-CCZnH2-Z.mjs → list-gy64AAWO.mjs} +2 -2
- package/dist/{list-D9EuxFHO.mjs → list-xfmTxHNu.mjs} +2 -2
- package/dist/{login-BzrAGJfu.mjs → login-C0VFlX8y.mjs} +4 -4
- package/dist/{logout-CKBiltoS.mjs → logout-BUgUTFol.mjs} +2 -2
- package/dist/measure-BmcmxRQ3.mjs +19 -0
- package/dist/{metadata-BcGcUEVJ.mjs → metadata-B1EmV1l5.mjs} +3 -3
- package/dist/{metadata-BjOrKtnv.mjs → metadata-Dwkcsa1c.mjs} +3 -3
- package/dist/{parse-id--iVTCKSo.mjs → parse-id-sKNP6yu5.mjs} +1 -1
- package/dist/{path-LgGU6Bd0.mjs → path-BZJR7YLR.mjs} +2 -2
- package/dist/{poll-BucRFJT-.mjs → poll-Cyw-dQi4.mjs} +1 -1
- package/dist/{poll-task-DTzKB3T3.mjs → poll-task-BG0oVnfZ.mjs} +2 -2
- package/dist/{preflight-CzqVX0PP.mjs → preflight-BjR63JnX.mjs} +1 -1
- package/dist/{query-BVZkK6Qk.mjs → query-C2xqnz24.mjs} +3 -3
- package/dist/{query-DG_jygDF.mjs → query-Dk55Wp2p.mjs} +4 -4
- package/dist/{remove-collection-HdeAfLyi.mjs → remove-collection-DbVNWGyb.mjs} +6 -6
- package/dist/{rescan-values-hWubCruZ.mjs → rescan-values-CuLQ9UHP.mjs} +3 -3
- package/dist/{run-C1-lDmQF.mjs → run-PDN33QjN.mjs} +5 -5
- package/dist/{runs-Q6DYQyqj.mjs → runs-B9T-7lei.mjs} +3 -3
- package/dist/{runtime-colqvhLf.mjs → runtime-DgHh4T6t.mjs} +1 -1
- package/dist/{schema-tables-BEastV_8.mjs → schema-tables-DjmNwrlT.mjs} +3 -3
- package/dist/{schemas-CgawwI_k.mjs → schemas-CWL8gv1C.mjs} +3 -3
- package/dist/{search-BWo7xSPP.mjs → search-D7JzmvfR.mjs} +3 -3
- package/dist/segment-DrZNuxsM.mjs +19 -0
- package/dist/{set-7Nm2ZTb_.mjs → set-C0dCLnUA.mjs} +4 -4
- package/dist/{setting-DSGXJehQ.mjs → setting-Licy7ZSs.mjs} +3 -3
- package/dist/{setup-aJLGLrIT.mjs → setup-BL4fqxoU.mjs} +4 -4
- package/dist/{skills-Q2AFsYvc.mjs → skills-Cn8FMCxG.mjs} +3 -3
- package/dist/snippet-C5_4nPGj.mjs +19 -0
- package/dist/{stash-DPQ0c-Cd.mjs → stash-SGod4B8-.mjs} +6 -6
- package/dist/{status-CvKPrV5X.mjs → status-BCb5iCsQ.mjs} +5 -5
- package/dist/{status-CvAATvV0.mjs → status-CvUuZVvC.mjs} +2 -2
- package/dist/{summary-CeOnoOq2.mjs → summary-qaTPTLWs.mjs} +3 -3
- package/dist/{sync-schema-aOPBc3CY.mjs → sync-schema-CjjAYTbO.mjs} +5 -5
- package/dist/table-DK1B8Hzq.mjs +19 -0
- package/dist/transform-Dh5746zH.mjs +24 -0
- package/dist/transform-job-D0nRGPYN.mjs +19 -0
- package/dist/{tree-MOQOBeAP.mjs → tree-C0MFcweC.mjs} +2 -2
- package/dist/{update-BWyCK8QV.mjs → update-BImaz9vj.mjs} +6 -6
- package/dist/{update-B6mg3AZD.mjs → update-BTzsE8GU.mjs} +5 -5
- package/dist/{update-BRrnfG0q.mjs → update-BUoWwmQV.mjs} +5 -5
- package/dist/{update-CitS-QRN.mjs → update-BqsGGrzx.mjs} +6 -6
- package/dist/{update-57uxZWcR.mjs → update-CHUUY-IB.mjs} +5 -5
- package/dist/{update-Bj9s0ri8.mjs → update-CJASCsG4.mjs} +5 -5
- package/dist/{update-DfNKr_vS.mjs → update-DCR0wFNl.mjs} +5 -5
- package/dist/{update-BkMWBzvk.mjs → update-Dty6LvU2.mjs} +5 -5
- package/dist/{update-dashcard-D_-ura3Y.mjs → update-dashcard-BkJaVVvf.mjs} +5 -5
- package/dist/{update-BD9xkglP.mjs → update-qoP7hZTg.mjs} +5 -5
- package/dist/{update-Dri4Zg2H.mjs → update-vk_M0eOR.mjs} +5 -5
- package/dist/{upgrade-CFkZ4USY.mjs → upgrade-TdaiAYbH.mjs} +2 -2
- package/dist/{uuid-DpinhSxA.mjs → uuid-BeUz8VpP.mjs} +2 -2
- package/dist/{values-BSS4DRxk.mjs → values-DR-3HcK6.mjs} +3 -3
- package/dist/{verify-B_A7v8TY.mjs → verify-D2uBlvmt.mjs} +1 -1
- package/dist/{wait-D3iSnjMM.mjs → wait-FAnqO-LT.mjs} +5 -5
- package/dist/{wait-flags-_LnHOeBA.mjs → wait-flags-B5BI_xob.mjs} +2 -2
- package/package.json +2 -1
- package/skill-data/core/SKILL.md +22 -23
- package/skill-data/data-analysis/SKILL.md +65 -0
- package/skill-data/data-transformation/SKILL.md +200 -0
- package/skill-data/document/SKILL.md +4 -4
- package/skill-data/mbql/SKILL.md +20 -20
- package/skill-data/robot-data-engineer/SKILL.md +142 -0
- package/skill-data/semantic-layer/SKILL.md +166 -0
- package/skill-data/transform/SKILL.md +46 -48
- package/skill-data/visualization/SKILL.md +5 -3
- package/skills/metabase-cli/SKILL.md +6 -0
- package/dist/add-collection-D9wXgmRj.mjs +0 -10
- package/dist/auth-cFC5m69m.mjs +0 -19
- package/dist/card-ClvGX6dQ.mjs +0 -20
- package/dist/collection-DjvowSJC.mjs +0 -20
- package/dist/dashboard-BFeURTOw.mjs +0 -21
- package/dist/db-CSH1kwQr.mjs +0 -22
- package/dist/document-KdT_Xj6r.mjs +0 -19
- package/dist/field-CTFnZI8G.mjs +0 -18
- package/dist/git-sync-C2vib8rx.mjs +0 -28
- package/dist/is-dirty-Bb0Rtj7x.mjs +0 -9
- package/dist/measure-Dw1QpRZa.mjs +0 -19
- package/dist/segment-DMuYvFjg.mjs +0 -19
- package/dist/snippet-Df2TrP7-.mjs +0 -19
- package/dist/table-pK4OkVtL.mjs +0 -19
- package/dist/transform-D60veFH8.mjs +0 -24
- 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
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
61
|
-
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 > /
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
> /
|
|
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
|
|
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 /
|
|
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 > /
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`.
|
|
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 };
|
package/dist/auth-cFC5m69m.mjs
DELETED
|
@@ -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 };
|
package/dist/card-ClvGX6dQ.mjs
DELETED
|
@@ -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 };
|
package/dist/db-CSH1kwQr.mjs
DELETED
|
@@ -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 };
|
package/dist/field-CTFnZI8G.mjs
DELETED
|
@@ -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 };
|