@oxy-hq/cli 0.7.0 → 0.9.0

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 (38) hide show
  1. package/README.md +131 -8
  2. package/dist/main.mjs +4909 -3551
  3. package/dist/main.mjs.map +1 -1
  4. package/dist/{mcp-xxWL3_dz.mjs → mcp-Bdp0rX0l.mjs} +2 -2
  5. package/dist/{mcp-xxWL3_dz.mjs.map → mcp-Bdp0rX0l.mjs.map} +1 -1
  6. package/package.json +5 -5
  7. package/skills/oxy-agents/SKILL.md +318 -0
  8. package/skills/oxy-agents/references/quick-reference.md +222 -0
  9. package/skills/oxy-agents/templates/agentic-template.yml +141 -0
  10. package/skills/oxy-airway-pipelines/SKILL.md +223 -316
  11. package/skills/oxy-automations/SKILL.md +61 -15
  12. package/skills/oxy-cli/SKILL.md +37 -4
  13. package/skills/oxy-custom-apps/SKILL.md +193 -291
  14. package/skills/oxy-custom-apps/references/minimal-starter.md +63 -0
  15. package/skills/oxy-custom-apps/references/performance-and-usage.md +105 -0
  16. package/skills/oxy-custom-apps/references/reading-data.md +106 -0
  17. package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +267 -0
  18. package/skills/oxy-custom-apps/references/verifying-in-the-browser.md +65 -0
  19. package/skills/oxy-data-apps/SKILL.md +362 -0
  20. package/skills/oxy-data-apps/references/examples.md +263 -0
  21. package/skills/oxy-data-apps/references/quick-reference.md +171 -0
  22. package/skills/oxy-data-apps/templates/dashboard-with-controls.app.yml +128 -0
  23. package/skills/oxy-data-apps/templates/simple-dashboard.app.yml +102 -0
  24. package/skills/oxy-data-architecture/SKILL.md +137 -152
  25. package/skills/oxy-evals/SKILL.md +362 -0
  26. package/skills/oxy-evals/references/diagnosis.md +243 -0
  27. package/skills/oxy-evals/references/results-json.md +145 -0
  28. package/skills/oxy-evals/references/writing-expected.md +112 -0
  29. package/skills/oxy-semantic-modeling/SKILL.md +239 -59
  30. package/skills/oxy-semantic-modeling/references/warehouse-casts.md +57 -0
  31. package/skills/oxy-semantic-modeling/templates/topic-template.yml +38 -0
  32. package/skills/oxy-semantic-modeling/templates/view-template.yml +119 -0
  33. package/template/.github/workflows/publish.yaml +15 -13
  34. package/template/.nvmrc +1 -1
  35. package/template/.oxyc-managed +5 -5
  36. package/template/README.md +21 -10
  37. package/template/package.json +3 -2
  38. package/template/pnpm-workspace.yaml +28 -17
@@ -0,0 +1,362 @@
1
+ ---
2
+ name: oxy-data-apps
3
+ description: >-
4
+ Use when creating, editing, reviewing, or debugging an Oxy Data App — a declarative `.app.yml`
5
+ file of tasks (`execute_sql`, `semantic_query`) and displays (tables, line / bar / pie charts,
6
+ markdown, side-by-side rows) — for a dashboard, report, KPI page, or data app, including
7
+ interactive filters: dropdowns, date pickers, toggles and other controls wired into SQL with
8
+ `{{ controls.x }}`. Also when an app renders blank or shows an error card, a chart has no data,
9
+ a control changes nothing, or an app is missing from the sidebar (`published:`). Not for
10
+ code-first React / Vite apps with an `oxy-app.json` manifest shipped with `oxyc publish` — those
11
+ are custom apps; use `oxy-custom-apps`.
12
+ ---
13
+
14
+ # Oxy Data Apps
15
+
16
+ A **Data App** is one YAML file, `<name>.app.yml`, in an Oxy workspace. Its
17
+ **tasks** fetch data; its **displays** render that data as tables, charts and
18
+ markdown; optional **controls** let a viewer filter it live. Oxy renders the
19
+ file directly — there is no build step and no front-end code.
20
+
21
+ ## Data app or custom app?
22
+
23
+ | | Data App (this skill) | Custom app |
24
+ | --- | --- | --- |
25
+ | What it is | Tasks + displays in one `.app.yml` | A React / TypeScript / Vite bundle |
26
+ | Identified by | the `.app.yml` file | an `oxy-app.json` manifest |
27
+ | Ships by | living in the workspace, `published: true` | `oxyc publish` |
28
+ | Use when | the page is tables, charts and markdown | the UI is bespoke |
29
+
30
+ Code-first React apps are a different product — see the `oxy-custom-apps` skill.
31
+
32
+ ## The file
33
+
34
+ ```yaml
35
+ title: Sales Overview # shown in listings; falls back to the filename
36
+ description: |
37
+ Revenue KPIs and the monthly trend.
38
+ published: true # default false = hidden from the sidebar
39
+
40
+ tasks: # required, at least one
41
+ - name: monthly_revenue
42
+ type: execute_sql
43
+ database: warehouse # a database name from config.yml
44
+ sql_query: |
45
+ SELECT DATE_TRUNC('month', order_date) AS month,
46
+ SUM(amount) AS revenue
47
+ FROM orders
48
+ GROUP BY 1
49
+ ORDER BY 1
50
+
51
+ display: # required, at least one
52
+ - type: line_chart
53
+ title: Revenue by month
54
+ data: monthly_revenue # a task name
55
+ x: month
56
+ y: revenue
57
+ y_format: currency
58
+ ```
59
+
60
+ - **Top-level keys are fixed:** `title`, `description`, `controls`, `tasks`,
61
+ `display`, `published`. Anything else is rejected.
62
+ - **Leave `name:` out.** The app's name is its filename (`sales.app.yml` →
63
+ `sales`). The server tolerates a `name:` key, but the published schema does
64
+ not list it, so `oxyc validate` reports it as an unknown field.
65
+ - **Where it can live:** any `**/*.app.yml` in the workspace, except under a
66
+ dot-prefixed directory or one named `target`, `node_modules`, `dist` or
67
+ `build`, and never with `.test.` in the filename. Those paths are skipped,
68
+ so the app silently does not exist.
69
+
70
+ ## Tasks
71
+
72
+ Every task has a unique `name` (start with a letter; letters, digits and
73
+ underscores) and a `type`. Two types carry almost every app:
74
+
75
+ ### `execute_sql`
76
+
77
+ ```yaml
78
+ - name: by_region
79
+ type: execute_sql
80
+ database: warehouse
81
+ sql_query: |
82
+ SELECT region, SUM(amount) AS total
83
+ FROM orders
84
+ GROUP BY region
85
+ ORDER BY total DESC
86
+ ```
87
+
88
+ - `database` must name a database in `config.yml` (`oxy validate` checks it).
89
+ - Use `sql_query` (inline) **or** `sql_file` (a workspace-relative path to a
90
+ `.sql` file). Inline SQL is the only form the browser can re-run when a
91
+ control changes; `sql_file` tasks always run on the server.
92
+ - Output columns are your `SELECT` aliases — that is what `x:`, `y:` and
93
+ friends reference.
94
+ - Results are capped at 10,000 rows. Aggregate in SQL; never ship raw rows to
95
+ a chart.
96
+
97
+ ### `semantic_query`
98
+
99
+ Asks the workspace's semantic model instead of writing SQL:
100
+
101
+ ```yaml
102
+ - name: revenue_trend
103
+ type: semantic_query
104
+ topic: sales
105
+ measures: [orders.total_revenue]
106
+ dimensions: [orders.region]
107
+ time_dimensions:
108
+ - dimension: orders.order_date
109
+ granularity: month # year|quarter|month|week|day|hour|minute|second
110
+ filters:
111
+ - field: orders.order_date
112
+ op: gte # eq neq gt gte lt lte take `value`
113
+ value: "2024-01-01" # in / not_in take `values: [...]`
114
+ orders:
115
+ - field: orders.order_date
116
+ direction: asc
117
+ limit: 500
118
+ ```
119
+
120
+ - **Output columns are `view__field`**: `orders__total_revenue`,
121
+ `orders__region`. A time dimension with a `granularity` gets one more
122
+ suffix: `orders__order_date__month`. Reference exactly those names from the
123
+ display, or the chart fails to bind its column.
124
+ - **Controls do not reach a `semantic_query`.** Its fields are sent as
125
+ written; `{{ controls.x }}` is not rendered there. A filter that must follow
126
+ a control has to be an `execute_sql` task.
127
+ - `in_date_range` / `not_in_date_range` filters take `from` and `to`.
128
+
129
+ ### Other task types — mostly don't
130
+
131
+ App tasks run through the automation runner, so the schema accepts every
132
+ automation task type. In a Data App:
133
+
134
+ - **`type: agent` does not work.** It validates, but a Data App runs its
135
+ tasks without an agent runtime, so the run fails with
136
+ `agent step '…' requires an InlineAgentRunner`. For AI commentary on the
137
+ data, point people at chat with an `.agentic.yml` agent instead (the
138
+ `oxy-agents` skill).
139
+ - **`type: workflow`** (the YAML keyword is still `workflow`) runs an
140
+ automation: `src: automations/report.automation.yml` plus optional
141
+ `variables:`. Its result reaches the app as **one text value**, not as
142
+ tables, so `data: report.some_step` passes validation (only the part before
143
+ the dot is checked) and renders nothing. Put the queries in the app as
144
+ `execute_sql` / `semantic_query` tasks instead.
145
+ - **`type: airway`** cannot run inside an app at all.
146
+ - A **misspelled `type:`** passes `oxy validate` and fails at run time with
147
+ `unknown task type`. `oxyc validate` does catch it.
148
+
149
+ ## Displays
150
+
151
+ Each `display:` entry is a block, rendered top to bottom. A block that fails
152
+ to parse becomes a single error card; the rest of the app still renders.
153
+
154
+ | `type` | Required | Optional |
155
+ | --- | --- | --- |
156
+ | `markdown` | `content` | — |
157
+ | `table` | `data` | `title`, `formats` |
158
+ | `line_chart` | `data`, `x`, `y` | `title`, `series`, `x_axis_label`, `y_axis_label`, `y_format` |
159
+ | `bar_chart` | `data`, `x`, `y` | `title`, `series`, `y_format` |
160
+ | `pie_chart` | `data`, `name`, `value` | `title`, `value_format` |
161
+ | `row` | `children` | `columns` (≥ 1; defaults to the number of children) |
162
+ | `control` / `controls` | see [Controls](#controls) | |
163
+
164
+ - **`data:` names a task.** `oxy validate` rejects a reference to a task that
165
+ does not exist.
166
+ - **`series:`** splits a line or bar chart into one series per value of that
167
+ column (multiple lines, grouped bars).
168
+ - **Number formats** — `y_format`, `value_format`, and the per-column
169
+ `formats:` map on a table take `currency`, `percent` (the value is already
170
+ 0–100, not a ratio) or `number` (thousands separators). Table `formats` keys
171
+ are output column names.
172
+ - **`markdown` has no `title`** (a `title:` there is silently ignored) — put a
173
+ heading in `content`. Its content is a template: `{{ task_name }}` inserts a
174
+ task's result, useful for a text result and useless for a table (it renders
175
+ as a JSON code block).
176
+ - **Axis labels exist only on `line_chart`.** On `bar_chart`,
177
+ `x_axis_label` / `y_axis_label` are silently ignored — no validator flags
178
+ them.
179
+ - **`row` children are displays, never controls.** A `type: control` inside a
180
+ row is not registered as a control and does not render.
181
+
182
+ ### Labels, not IDs
183
+
184
+ Never put an opaque key (`customer_id`, a UUID) on a chart axis, a pie
185
+ `name:`, or as the only label in a table. Pull a readable name instead:
186
+ through the semantic model (a joined view's name dimension, e.g.
187
+ `stores__store_name` — the `oxy-semantic-modeling` skill covers foreign
188
+ entities), or with an explicit `JOIN` in `execute_sql`. If no name exists
189
+ anywhere, keep the ID but say so in a markdown note.
190
+
191
+ ## Controls
192
+
193
+ Controls are dropdowns, date pickers and toggles rendered as a bar above the
194
+ app. Changing one re-runs every task whose SQL reads it.
195
+
196
+ | Kind | Widget | Value |
197
+ | --- | --- | --- |
198
+ | `select` | dropdown | string |
199
+ | `date` | date picker | string `YYYY-MM-DD` |
200
+ | `toggle` | on/off switch | boolean |
201
+
202
+ ### Declaring them
203
+
204
+ There are three equivalent places. Pick one per app — they are merged, so a
205
+ control declared twice shows up twice.
206
+
207
+ ```yaml
208
+ display:
209
+ # 1. Inline, one block per control (recommended). The kind is `control_type:`.
210
+ - type: control
211
+ name: region
212
+ control_type: select # NOT `type:` — `type: control` already took that key
213
+ label: Region
214
+ options: [All, North, South]
215
+ default: All
216
+
217
+ # 2. Inline group. Items use plain `type:`.
218
+ - type: controls
219
+ items:
220
+ - name: start_date
221
+ type: date
222
+ default: "2024-01-01"
223
+
224
+ # 3. Top-level array, beside tasks/display. Items use plain `type:`.
225
+ controls:
226
+ - name: holidays_only
227
+ type: toggle
228
+ default: false
229
+ ```
230
+
231
+ Fields: `name` (required; read as `controls.<name>`), the kind (`control_type`
232
+ inline, `type` otherwise), `label` (defaults to the name), `default`,
233
+ `options` (static list) **or** `source` (a task whose **first column** fills
234
+ the dropdown — include a sentinel such as `All` in that query yourself).
235
+
236
+ `default:` and `options:` may be Jinja evaluated once when the app loads —
237
+ `default: "{{ now(fmt='%Y-%m-%d') }}"`. A value a viewer sends is data, never
238
+ a template. An empty value falls back to the default.
239
+
240
+ ### Using them in SQL
241
+
242
+ ```sql
243
+ SELECT category, SUM(revenue) AS total
244
+ FROM sales
245
+ WHERE sale_date >= {{ controls.start_date | sqlquote }}
246
+ AND ({{ controls.region | sqlquote }} = 'All'
247
+ OR region = {{ controls.region | sqlquote }})
248
+ {% if controls.holidays_only %}AND period = 'Holiday'{% endif %}
249
+ GROUP BY category
250
+ ```
251
+
252
+ 1. **Pipe every string and date through `| sqlquote`.** It adds the quotes
253
+ and escapes embedded ones.
254
+ 2. **Never add quotes around it** — `'{{ controls.x | sqlquote }}'` produces
255
+ `''value''`.
256
+ 3. **Optional filters use a sentinel.** A `select` always has a value, so
257
+ offer `All` and write `(… = 'All' OR col = …)`.
258
+ 4. **Toggles go in `{% if controls.flag %}…{% endif %}`.**
259
+ 5. **Dates are strings** — compare directly or `CAST(… AS DATE)`.
260
+
261
+ ### Where a re-run happens (`mode`)
262
+
263
+ Each task takes `mode: client` (the default) or `mode: server`.
264
+
265
+ - **Client** — the browser re-runs the SQL in an in-browser DuckDB. It only
266
+ applies when **every** task feeding a display is an inline `execute_sql`
267
+ against a **local DuckDB** database, reading quoted files
268
+ (`FROM 'sales.csv'`) or inline `VALUES`. Each task runs alone, so one task's
269
+ SQL cannot read another task's result.
270
+ - **Server** — everything else, automatically: warehouses, DuckLake,
271
+ `sql_file`, `semantic_query`. Setting `mode: server` is always safe.
272
+ - The browser understands only four Jinja forms: `{{ controls.x }}`,
273
+ `{{ controls.x | sqlquote }}`, `{{ controls.x | default('v') }}`, and
274
+ `{% if controls.x %}…{% endif %}`. Anything else — `{% else %}`, loops,
275
+ comparisons — makes the browser give up and re-run on the server for the
276
+ rest of the session. It still works, just slower; put comparison logic in
277
+ SQL (`CASE`, `OR`) to keep the fast path.
278
+
279
+ ## Publishing and visibility
280
+
281
+ - **`published` defaults to `false`, and an unpublished app is hidden from the
282
+ sidebar.** Authors still open, edit and run it from the IDE, so "it works
283
+ for me but nobody can find it" is almost always this line.
284
+ - Publish by setting `published: true`, or with the IDE's publish toggle,
285
+ which writes that same line into the file. Changing it needs an editing
286
+ role; Viewers cannot.
287
+ - There is nothing to upload. `oxyc publish` is for custom apps and has
288
+ nothing to do with Data Apps.
289
+
290
+ ## Building one
291
+
292
+ For a new app, plan before writing YAML, and confirm the plan with the user:
293
+
294
+ ```text
295
+ App: <file name> — <one-sentence purpose>
296
+ Data: <database.table or semantic topic>
297
+ Tasks: <task_name>: <intent> -> columns <a, b, c>
298
+ Displays: <type>: <what it shows> (data: <task_name>)
299
+ ```
300
+
301
+ 1. **Profile before charting.** Before you commit to a trend or a ranking,
302
+ check that the data supports it — enough rows, at least three months for a
303
+ trend, a measure that actually varies, an entity count worth ranking. The
304
+ profiling query is in [the quick reference](references/quick-reference.md#profiling-query).
305
+ 2. **Write the file.** Start from a template (below), match the database's SQL
306
+ dialect, alias every output column, `ORDER BY` anything a chart draws.
307
+ 3. **Validate.**
308
+ - `oxyc validate --file apps/sales.app.yml` — offline schema check;
309
+ catches unknown keys, a wrong `type`, `type:` vs `control_type:`.
310
+ - `oxy validate --file apps/sales.app.yml` — also checks that each
311
+ `database:` exists in `config.yml` and each display's `data:` names a
312
+ task.
313
+ - Neither runs SQL, checks a `{{ controls.x }}` reference against the
314
+ declared controls, or checks that `source:` names a task.
315
+ 4. **Run the queries.** Copy a task's SQL into a scratch file with literal
316
+ values in place of the controls, then
317
+ `oxy run scratch.sql --database warehouse`. For a referenced automation,
318
+ `oxy run automations/report.automation.yml`.
319
+ 5. **Open the app** (locally: `oxy start --enterprise`, then the IDE).
320
+ Every block should be populated; a failing task shows an error naming it.
321
+ Change each control and watch its tasks re-run.
322
+
323
+ **When something fails, fix it once.** Read the error and make one targeted
324
+ change — restate a `JOIN … ON`, swap a function for the dialect's equivalent,
325
+ drop a measure that will not resolve — and re-run. If it fails again, stop and
326
+ report the error verbatim; do not loop on guesses. If a profiling query fails
327
+ twice, drop that chart rather than ship a misleading one.
328
+
329
+ **Editing an existing app:** read it, say what it does now, propose the
330
+ smallest change, and keep its existing style and naming.
331
+
332
+ ## Pitfalls
333
+
334
+ | Symptom | Cause | Fix |
335
+ | --- | --- | --- |
336
+ | App missing from the sidebar | `published` is false (the default) | `published: true`, or the IDE publish toggle |
337
+ | App does not exist at all | path under a dot dir / `build` / `dist`, or `.test.` in the name | move or rename it |
338
+ | `oxyc validate`: unknown field `name` | top-level `name:` | delete it; the filename is the name |
339
+ | Chart blank, column not found | `semantic_query` column referenced without `view__` or the `__<granularity>` suffix | use the exact output column name |
340
+ | Control renders but changes nothing | its value is not used in any `execute_sql`, or it feeds a `semantic_query` | template it into `execute_sql` SQL |
341
+ | `type: select` inside `- type: control` rejected | the inline form uses `control_type:` | `control_type: select` |
342
+ | SQL error near `''value''` | quotes added around `sqlquote` | remove your quotes |
343
+ | Control inside a `row` never appears | rows only hold displays | move it to the top level of `display:` |
344
+ | `requires an InlineAgentRunner` | `type: agent` task | remove it; use chat with an agent |
345
+ | `data: task.step` renders nothing | sub-automation results arrive as one text value | query directly in the app |
346
+ | `unknown task type` at run time | misspelled task `type:` | fix it; `oxyc validate` catches this |
347
+ | Dashboard slow on every control change | browser path not possible, or rare Jinja forced the server | keep to the four Jinja forms; aggregate in SQL |
348
+ | Chart axis is a list of UUIDs | key column on `x:` / `name:` | join to a readable name |
349
+
350
+ ## Read next
351
+
352
+ - [references/quick-reference.md](references/quick-reference.md) — every field
353
+ of every task, display and control; dialect cheatsheet; checklist.
354
+ - [references/examples.md](references/examples.md) — prompts turned into plans
355
+ and finished apps, plus a minimal edit.
356
+ - [templates/simple-dashboard.app.yml](templates/simple-dashboard.app.yml) —
357
+ KPIs, a trend, and a breakdown.
358
+ - [templates/dashboard-with-controls.app.yml](templates/dashboard-with-controls.app.yml)
359
+ — all three control kinds, a data-driven dropdown, and a row layout.
360
+ - Related skills: `oxy-semantic-modeling` (views and topics a `semantic_query`
361
+ reads), `oxy-automations` (`.automation.yml`), `oxy-agents` (`.agentic.yml`),
362
+ `oxy-custom-apps` (code-first apps).
@@ -0,0 +1,263 @@
1
+ # Data App examples
2
+
3
+ Requests turned into a plan and a finished `.app.yml`. The SQL is
4
+ Postgres / DuckDB / Snowflake dialect; see the dialect cheatsheet in
5
+ [quick-reference.md](quick-reference.md#sql-dialect-cheatsheet) for BigQuery and
6
+ ClickHouse. `warehouse` stands for whatever database name `config.yml` uses.
7
+
8
+ ## 1. KPIs and a monthly trend
9
+
10
+ **Request:** "Create a sales dashboard with total revenue, number of orders,
11
+ and a monthly revenue trend, from the orders table."
12
+
13
+ **Questions worth asking first:** what period should it cover? Any breakdown
14
+ (region, category)? Which column is the order date?
15
+
16
+ **Plan:**
17
+
18
+ ```text
19
+ App: sales_dashboard.app.yml — sales KPIs and the monthly revenue trend
20
+ Data: warehouse.orders
21
+ Tasks:
22
+ kpis: headline numbers -> total_revenue, order_count, avg_order_value
23
+ monthly_trend: revenue per month -> month, revenue
24
+ Displays:
25
+ markdown: title and one-line intro
26
+ table: kpis (currency formats)
27
+ line_chart: monthly_trend, x=month, y=revenue
28
+ ```
29
+
30
+ **`sales_dashboard.app.yml`:**
31
+
32
+ ```yaml
33
+ title: Sales Dashboard
34
+ description: Sales KPIs and the monthly revenue trend.
35
+ published: true
36
+
37
+ tasks:
38
+ - name: kpis
39
+ type: execute_sql
40
+ database: warehouse
41
+ sql_query: |
42
+ SELECT
43
+ ROUND(SUM(amount), 2) AS total_revenue,
44
+ COUNT(*) AS order_count,
45
+ ROUND(AVG(amount), 2) AS avg_order_value
46
+ FROM orders
47
+
48
+ - name: monthly_trend
49
+ type: execute_sql
50
+ database: warehouse
51
+ sql_query: |
52
+ SELECT
53
+ DATE_TRUNC('month', order_date) AS month,
54
+ ROUND(SUM(amount), 2) AS revenue
55
+ FROM orders
56
+ GROUP BY 1
57
+ ORDER BY 1
58
+
59
+ display:
60
+ - type: markdown
61
+ content: |
62
+ # Sales Dashboard
63
+
64
+ Overview of sales performance.
65
+
66
+ - type: table
67
+ title: Key metrics
68
+ data: kpis
69
+ formats:
70
+ total_revenue: currency
71
+ avg_order_value: currency
72
+ order_count: number
73
+
74
+ - type: line_chart
75
+ title: Monthly revenue
76
+ data: monthly_trend
77
+ x: month
78
+ y: revenue
79
+ x_axis_label: Month
80
+ y_axis_label: Revenue
81
+ y_format: currency
82
+ ```
83
+
84
+ ## 2. Segments: share, detail, and trend
85
+
86
+ **Request:** "Show revenue by customer segment as a pie chart, a breakdown
87
+ table, and how each segment trends over time."
88
+
89
+ **Plan:**
90
+
91
+ ```text
92
+ App: customer_segments.app.yml — revenue distribution and trend by segment
93
+ Data: warehouse.customers joined to warehouse.orders
94
+ Tasks:
95
+ segment_summary: revenue by segment -> segment, revenue, customer_count, revenue_per_customer
96
+ segment_trends: monthly, per segment -> month, segment, revenue
97
+ Displays:
98
+ row: pie_chart (segment_summary) + table (segment_summary)
99
+ line_chart: segment_trends, series=segment
100
+ ```
101
+
102
+ **`customer_segments.app.yml`:**
103
+
104
+ ```yaml
105
+ title: Customer Segments
106
+ description: Revenue distribution and trends across customer segments.
107
+ published: true
108
+
109
+ tasks:
110
+ - name: segment_summary
111
+ type: execute_sql
112
+ database: warehouse
113
+ sql_query: |
114
+ SELECT
115
+ c.segment,
116
+ ROUND(SUM(o.amount), 2) AS revenue,
117
+ COUNT(DISTINCT c.customer_id) AS customer_count,
118
+ ROUND(SUM(o.amount) / COUNT(DISTINCT c.customer_id), 2) AS revenue_per_customer
119
+ FROM customers c
120
+ JOIN orders o ON o.customer_id = c.customer_id
121
+ GROUP BY c.segment
122
+ ORDER BY revenue DESC
123
+
124
+ - name: segment_trends
125
+ type: execute_sql
126
+ database: warehouse
127
+ sql_query: |
128
+ SELECT
129
+ DATE_TRUNC('month', o.order_date) AS month,
130
+ c.segment,
131
+ ROUND(SUM(o.amount), 2) AS revenue
132
+ FROM customers c
133
+ JOIN orders o ON o.customer_id = c.customer_id
134
+ GROUP BY 1, 2
135
+ ORDER BY 1, 2
136
+
137
+ display:
138
+ - type: markdown
139
+ content: |
140
+ # Customer Segments
141
+
142
+ - type: row
143
+ columns: 2
144
+ children:
145
+ - type: pie_chart
146
+ title: Revenue share
147
+ data: segment_summary
148
+ name: segment # a readable label, never customer_id
149
+ value: revenue
150
+ value_format: currency
151
+ - type: table
152
+ title: Segment summary
153
+ data: segment_summary
154
+ formats:
155
+ revenue: currency
156
+ revenue_per_customer: currency
157
+
158
+ - type: line_chart
159
+ title: Monthly revenue by segment
160
+ data: segment_trends
161
+ x: month
162
+ y: revenue
163
+ series: segment # one line per segment
164
+ y_format: currency
165
+ ```
166
+
167
+ ## 3. From the semantic model
168
+
169
+ **Request:** "Build a revenue report from our sales topic — monthly revenue
170
+ and revenue by store."
171
+
172
+ When the workspace has a semantic model, prefer `semantic_query`: the
173
+ measures are defined once and the app cannot drift from them. Output columns
174
+ are `view__field`, plus `__<granularity>` on a bucketed time dimension.
175
+
176
+ **`revenue_report.app.yml`:**
177
+
178
+ ```yaml
179
+ title: Revenue Report
180
+ published: true
181
+
182
+ tasks:
183
+ - name: revenue_by_month
184
+ type: semantic_query
185
+ topic: sales
186
+ measures: [orders.total_revenue]
187
+ time_dimensions:
188
+ - dimension: orders.order_date
189
+ granularity: month
190
+ filters:
191
+ - field: orders.order_date
192
+ op: gte
193
+ value: "2024-01-01"
194
+ orders:
195
+ - field: orders.order_date
196
+ direction: asc
197
+
198
+ - name: revenue_by_store
199
+ type: semantic_query
200
+ topic: sales
201
+ measures: [orders.total_revenue]
202
+ dimensions: [stores.store_name] # the readable name via a joined view, not orders.store_id
203
+ orders:
204
+ - field: orders.total_revenue
205
+ direction: desc
206
+ limit: 20
207
+
208
+ display:
209
+ - type: line_chart
210
+ title: Revenue by month
211
+ data: revenue_by_month
212
+ x: orders__order_date__month # granularity suffix is required
213
+ y: orders__total_revenue
214
+ y_format: currency
215
+
216
+ - type: bar_chart
217
+ title: Top 20 stores
218
+ data: revenue_by_store
219
+ x: stores__store_name
220
+ y: orders__total_revenue
221
+ y_format: currency
222
+ ```
223
+
224
+ If a chart should follow a control, the semantic query cannot do it —
225
+ controls are not rendered into `semantic_query` fields. Use an `execute_sql`
226
+ task for that chart.
227
+
228
+ ## 4. A minimal edit
229
+
230
+ **Request:** "Add a bar chart of revenue by product category to
231
+ `sales_report.app.yml`."
232
+
233
+ 1. Read the file and say what it does now ("monthly trend and KPIs from
234
+ `orders`").
235
+ 2. Propose the smallest change — one task, one display — and keep the file's
236
+ existing naming and database.
237
+ 3. Apply it, then validate and open the app.
238
+
239
+ ```yaml
240
+ # tasks: — add
241
+ - name: revenue_by_category
242
+ type: execute_sql
243
+ database: warehouse
244
+ sql_query: |
245
+ SELECT product_category, ROUND(SUM(amount), 2) AS revenue
246
+ FROM orders
247
+ GROUP BY product_category
248
+ ORDER BY revenue DESC
249
+
250
+ # display: — add after the trend chart
251
+ - type: bar_chart
252
+ title: Revenue by product category
253
+ data: revenue_by_category
254
+ x: product_category
255
+ y: revenue
256
+ y_format: currency
257
+ ```
258
+
259
+ ## Controls
260
+
261
+ The full interactive pattern — `select` filled from a task, `date`, `toggle`,
262
+ SQL templating and a row layout — is
263
+ [templates/dashboard-with-controls.app.yml](../templates/dashboard-with-controls.app.yml).