@oxy-hq/cli 0.8.0 → 0.10.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.
- package/README.md +312 -31
- package/dist/main.mjs +11236 -2822
- package/dist/main.mjs.map +1 -1
- package/dist/{mcp-xxWL3_dz.mjs → mcp-B2KBWNOU.mjs} +86 -3
- package/dist/mcp-B2KBWNOU.mjs.map +1 -0
- package/package.json +6 -5
- package/skills/oxy-agents/SKILL.md +318 -0
- package/skills/oxy-agents/references/quick-reference.md +222 -0
- package/skills/oxy-agents/templates/agentic-template.yml +141 -0
- package/skills/oxy-airway-pipelines/SKILL.md +223 -316
- package/skills/oxy-automations/SKILL.md +61 -15
- package/skills/oxy-cli/SKILL.md +40 -6
- package/skills/oxy-custom-apps/SKILL.md +380 -294
- package/skills/oxy-custom-apps/references/minimal-starter.md +71 -0
- package/skills/oxy-custom-apps/references/performance-and-usage.md +151 -0
- package/skills/oxy-custom-apps/references/reading-data.md +106 -0
- package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +307 -0
- package/skills/oxy-custom-apps/references/verifying-in-the-browser.md +65 -0
- package/skills/oxy-data-apps/SKILL.md +362 -0
- package/skills/oxy-data-apps/references/examples.md +263 -0
- package/skills/oxy-data-apps/references/quick-reference.md +171 -0
- package/skills/oxy-data-apps/templates/dashboard-with-controls.app.yml +128 -0
- package/skills/oxy-data-apps/templates/simple-dashboard.app.yml +102 -0
- package/skills/oxy-data-architecture/SKILL.md +137 -152
- package/skills/oxy-evals/SKILL.md +362 -0
- package/skills/oxy-evals/references/diagnosis.md +243 -0
- package/skills/oxy-evals/references/results-json.md +145 -0
- package/skills/oxy-evals/references/writing-expected.md +112 -0
- package/skills/oxy-semantic-modeling/SKILL.md +239 -59
- package/skills/oxy-semantic-modeling/references/warehouse-casts.md +57 -0
- package/skills/oxy-semantic-modeling/templates/topic-template.yml +38 -0
- package/skills/oxy-semantic-modeling/templates/view-template.yml +119 -0
- package/template/.claude/hooks/oxyc-cloud-session.sh +66 -0
- package/template/.claude/settings.json +16 -0
- package/template/.github/workflows/publish.yaml +15 -13
- package/template/.nvmrc +1 -1
- package/template/.oxyc-managed +18 -5
- package/template/README.md +21 -10
- package/template/package.json +3 -2
- package/template/pnpm-workspace.yaml +28 -17
- package/dist/mcp-xxWL3_dz.mjs.map +0 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Verify it in the browser (CI does not check a published bundle)
|
|
2
|
+
|
|
3
|
+
Nothing checks a bundle before it reaches users. `tsc` and `vite build` pass on
|
|
4
|
+
a card rendering a red error banner, a grid with ragged rows, a button wired to
|
|
5
|
+
nothing, or a component misreading its function's response shape. **The build is
|
|
6
|
+
not the check — driving the published app is.**
|
|
7
|
+
|
|
8
|
+
## An agent on a sandbox agent token cannot do this on a cloud target
|
|
9
|
+
|
|
10
|
+
And says so. The token signs no browser in. Your sandbox's page is on its own
|
|
11
|
+
host (the `url` from `env create`), behind a staff session, and `oxyc proxy`
|
|
12
|
+
cannot target a sandbox. Verify the functions in the sandbox, hand your
|
|
13
|
+
engineer the link, and list what on the page you could not check
|
|
14
|
+
([What to hand back](sandbox-agent-loop.md#what-to-hand-back)). The rest of
|
|
15
|
+
this file is what they do with that link.
|
|
16
|
+
|
|
17
|
+
## Drive the page
|
|
18
|
+
|
|
19
|
+
Use a browser-automation MCP server — the Playwright one (`@playwright/mcp`)
|
|
20
|
+
fits. After `oxyc publish`, for anything touching `src/**` or a function a
|
|
21
|
+
card calls:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
browser_navigate → the published /customer-apps/<org>/<slug>/ URL
|
|
25
|
+
browser_find → locate a control by its text, get its ref
|
|
26
|
+
browser_click → drive it
|
|
27
|
+
browser_evaluate → MEASURE (grid widths, computed styles, element counts)
|
|
28
|
+
browser_take_screenshot → look, and compare before/after for visual work
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Calling the function endpoint is not verifying the UI.** A bearer-token
|
|
32
|
+
`fetch` proves the handler returns JSON. It does not prove the card renders it,
|
|
33
|
+
that the button is wired, or that a timer stops when told — and it can mislead
|
|
34
|
+
outright: a bearer-token call and a browser session don't always resolve the
|
|
35
|
+
same app state, so such a check can "pass" against data the page never shows.
|
|
36
|
+
If the claim is about what a person sees, drive the page.
|
|
37
|
+
|
|
38
|
+
`browser_evaluate` is the underused one. Design questions are usually
|
|
39
|
+
measurable, and measuring beats squinting:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// Ragged rows? Sum each grid row's children against the container.
|
|
43
|
+
const grid = document.querySelector("main > div");
|
|
44
|
+
const rows = {};
|
|
45
|
+
for (const el of grid.children) {
|
|
46
|
+
const b = el.getBoundingClientRect();
|
|
47
|
+
(rows[Math.round(b.top)] ||= []).push(Math.round(b.width));
|
|
48
|
+
}
|
|
49
|
+
// any row summing well under the container width is a gap
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Two prerequisites that bite
|
|
53
|
+
|
|
54
|
+
- **The app is behind auth.** The browser must hold a session for the target
|
|
55
|
+
Oxy deployment — sign in there first.
|
|
56
|
+
- **`pnpm dev` against cloud data returns `origin not allowed`** — the data API
|
|
57
|
+
rejects a localhost origin for a published app. Drive the published URL, or
|
|
58
|
+
run the `oxyc proxy` sidecar
|
|
59
|
+
([reading-data.md](reading-data.md#working-locally-against-cloud-data-oxyc-proxy)).
|
|
60
|
+
|
|
61
|
+
## Check the whole page
|
|
62
|
+
|
|
63
|
+
Not just the card you changed. A shared component (a layout wrapper, a design
|
|
64
|
+
primitive) touches every card, and a section you didn't open is where the
|
|
65
|
+
regression hides.
|
|
@@ -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).
|