@oxy-hq/cli 0.1.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 +163 -0
- package/dist/main.mjs +6688 -0
- package/dist/main.mjs.map +1 -0
- package/dist/mcp-VdontkUr.mjs +316 -0
- package/dist/mcp-VdontkUr.mjs.map +1 -0
- package/json-schemas/agent-test.json +104 -0
- package/json-schemas/agentic.json +406 -0
- package/json-schemas/app.json +1884 -0
- package/json-schemas/config.json +2215 -0
- package/json-schemas/flow-test.json +124 -0
- package/json-schemas/global-semantics.json +782 -0
- package/json-schemas/workflow.json +1907 -0
- package/package.json +65 -0
- package/skills/oxy-airway-pipelines/SKILL.md +338 -0
- package/skills/oxy-automations/SKILL.md +67 -0
- package/skills/oxy-cli/SKILL.md +134 -0
- package/skills/oxy-custom-apps/SKILL.md +330 -0
- package/skills/oxy-data-architecture/SKILL.md +225 -0
- package/skills/oxy-semantic-modeling/SKILL.md +216 -0
- package/template/.gitattributes +2 -0
- package/template/.github/scripts/ci-publish.sh +77 -0
- package/template/.github/scripts/discover-apps.sh +175 -0
- package/template/.github/scripts/publish-env.sh +55 -0
- package/template/.github/workflows/publish.yaml +653 -0
- package/template/.github/workflows/validate.yaml +264 -0
- package/template/.nvmrc +1 -0
- package/template/.oxyc-managed +297 -0
- package/template/README.md +414 -0
- package/template/_gitignore +32 -0
- package/template/apps/.gitkeep +0 -0
- package/template/config.yml +53 -0
- package/template/memory/.gitkeep +0 -0
- package/template/package.json +17 -0
- package/template/pipelines/.gitkeep +0 -0
- package/template/pnpm-workspace.yaml +32 -0
- package/template/scripts/dev.sh +443 -0
- package/template/semantics/topics/.gitkeep +0 -0
- package/template/semantics/views/.gitkeep +0 -0
- package/template/workflows/.gitkeep +0 -0
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oxy-hq/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "oxyc \u2014 the Oxy CLI: a gh-api-style client for the Oxy HTTP API, plus the customer-account tooling that scopes a session to one customer.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"oxyc": "./dist/main.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"README.md",
|
|
11
|
+
"dist",
|
|
12
|
+
"json-schemas",
|
|
13
|
+
"skills",
|
|
14
|
+
"template"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"registry": "https://registry.npmjs.org",
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/oxy-hq/oxygen-internal.git",
|
|
23
|
+
"directory": "sdk/cli"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"codegen": "node scripts/sync-schemas.mjs && node scripts/embed-assets.mjs",
|
|
27
|
+
"prebuild": "pnpm run codegen",
|
|
28
|
+
"build": "tsdown",
|
|
29
|
+
"build:watch": "tsdown --watch",
|
|
30
|
+
"build:binary": "pnpm run build && node scripts/build-binaries.mjs",
|
|
31
|
+
"lint": "biome check src",
|
|
32
|
+
"prepublishOnly": "pnpm run build",
|
|
33
|
+
"pretest": "pnpm run build",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest",
|
|
36
|
+
"pretypecheck": "pnpm run codegen",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"oxy",
|
|
41
|
+
"cli",
|
|
42
|
+
"api",
|
|
43
|
+
"agent"
|
|
44
|
+
],
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@clack/prompts": "^0.11.0",
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
|
+
"ajv": "^8.20.0",
|
|
50
|
+
"ajv-formats": "^3.0.1",
|
|
51
|
+
"commander": "^14.0.2",
|
|
52
|
+
"picocolors": "^1.1.1",
|
|
53
|
+
"yaml": "^2.8.1",
|
|
54
|
+
"zod": "^3.25.28 || ^4"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^26.2.0",
|
|
58
|
+
"tsdown": "^0.22.14",
|
|
59
|
+
"typescript": "^7.0.2",
|
|
60
|
+
"vitest": "^4.1.11"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=20"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oxy-airway-pipelines
|
|
3
|
+
description: Use when writing, reviewing, or debugging an Oxy Airway ELT pipeline (`.airway.yml`) — sources, the `kind` values Oxy actually wires, credentials, reset and retry — or when one misbehaves: "unsupported source kind", a pipeline that parses then fails at run time, credentials that do not resolve, a *pipeline* run landing `completed_with_errors` (an *automation* run with that status is `oxy-automations`), a schema that will not change, or duplicate rows. Airway has no published docs; this skill is the reference.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Oxy Airway pipelines
|
|
7
|
+
|
|
8
|
+
**Unlike every other Oxy subsystem, Airway has no published documentation** — no
|
|
9
|
+
`airway.mdx`, no `.airway.yml` reference page, no JSON schema. So this skill
|
|
10
|
+
carries content instead of pointing at a doc, which also makes it the
|
|
11
|
+
fastest-rotting thing in this set. Treat it as a map, not the territory.
|
|
12
|
+
|
|
13
|
+
## Version anchor, and what is actually authoritative
|
|
14
|
+
|
|
15
|
+
Everything below was read on 2026-08-17/18 against:
|
|
16
|
+
|
|
17
|
+
| | |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `oxy-internal` workspace version | **0.5.126** (`Cargo.toml:522`) |
|
|
20
|
+
| `airway` engine dependency | git tag **0.1.30** (declared in `Cargo.toml`, the `airway = { git = …, tag = … }` line — `:509` today) resolving to rev `7c1737af9e69e0f9ad4e7a26a7969e4d1309538a` (the rev lives **only** in `Cargo.lock`, `[[package]] name = "airway"` — `:731` today; `Cargo.toml` never names it) |
|
|
21
|
+
| Real corpus | Poke House's 19 `oxy/pipelines/*.airway.yml` |
|
|
22
|
+
|
|
23
|
+
**Bumped since.** On 2026-08-19 `Cargo.lock` reads airway **0.1.32**
|
|
24
|
+
(`55e2794490ebcfffdf69a9693a6044efe313d702`). Everything below was read at
|
|
25
|
+
0.1.30 and wants re-checking against the live `source_factory.rs`. One error has
|
|
26
|
+
since been corrected in place (the `replacing` buffer's location, under
|
|
27
|
+
duplicate rows) — note that it was **not** caused by the bump: it was wrong at
|
|
28
|
+
0.1.30 too. A stale claim here is at least as likely to be a misreading as it is
|
|
29
|
+
to be drift; check which before blaming the pin.
|
|
30
|
+
|
|
31
|
+
Airway is an **external engine fetched by git tag**, not a vendored copy —
|
|
32
|
+
`crates/agentic/airway` is only Oxy's wiring around it. So a dependency bump can
|
|
33
|
+
change which `kind`s work, and how they behave, with no oxy-internal commit at
|
|
34
|
+
all. Re-check `Cargo.lock` before trusting any list here.
|
|
35
|
+
|
|
36
|
+
**Two files are authoritative. When they disagree with this skill, they win:**
|
|
37
|
+
|
|
38
|
+
- **`crates/agentic/airway/src/config.rs`** — the parser. Which keys exist,
|
|
39
|
+
which are required, what the defaults are.
|
|
40
|
+
- **`crates/agentic/airway/src/source_factory.rs`** — the dispatch. Which
|
|
41
|
+
`kind` values Oxy can actually build. (Changing that crate rather than using
|
|
42
|
+
it? Read its `CLAUDE.md` first.)
|
|
43
|
+
|
|
44
|
+
**How the "no published doc" claim was established, so you can redo it:** the
|
|
45
|
+
live docs sitemap — <https://www.oxygen-hq.com/docs/sitemap.xml> — carries 146
|
|
46
|
+
URLs, **zero** mentioning airway, and `docs/build/data-infrastructure/` covers
|
|
47
|
+
three of the four similarly-named engines (Airform, airlayer, Airhouse) but not
|
|
48
|
+
this one. The single docs page whose URL says "pipeline",
|
|
49
|
+
<https://www.oxygen-hq.com/docs/guide/build/agents/pipeline>, is a same-word
|
|
50
|
+
false positive documenting the **agentic reasoning FSM**, not ELT. Do not send
|
|
51
|
+
anyone there for Airway.
|
|
52
|
+
|
|
53
|
+
## Anatomy
|
|
54
|
+
|
|
55
|
+
`AirwayPipelineSpec` (`config.rs`) is `#[serde(deny_unknown_fields)]`, so an
|
|
56
|
+
unrecognised top-level key is a **hard parse error**, not a silent no-op. That
|
|
57
|
+
is the friendly half; the traps below are the rest.
|
|
58
|
+
|
|
59
|
+
| Key | Required | Default | Notes |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `name` | **yes** | — | validated non-empty |
|
|
62
|
+
| `source` | **yes** | — | `{kind: String, config: <free-form>}` |
|
|
63
|
+
| `destination` | **yes** | — | `{database, dataset_name, schema_separator?}` |
|
|
64
|
+
| `description` | no | — | free text; `>` block scalar is house style |
|
|
65
|
+
| `resources` | no | `[]` = all | subset of the source's advertised resources |
|
|
66
|
+
| `concurrency` | no | **`1`** | validated, capped at `MAX_CONCURRENCY = 16` |
|
|
67
|
+
| `allow_concurrent_runs` | no | `false` | single-flight; see run semantics below |
|
|
68
|
+
| `streaming` | no | `true` | falls back to the bulk path transparently |
|
|
69
|
+
| `channel_capacity` | no | `2 * concurrency` | streaming path only |
|
|
70
|
+
|
|
71
|
+
The corpus's smallest file (`quickbooks_financials.airway.yml`; IDs redacted and
|
|
72
|
+
comments stripped — the real file's comments cover `database:` resolving
|
|
73
|
+
credentials at run time via `airhouse_managed` ephemeral creds, plus commented
|
|
74
|
+
`resources:` / `streaming:` exemplars):
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
name: quickbooks_financials
|
|
78
|
+
source:
|
|
79
|
+
kind: quickbooks
|
|
80
|
+
config:
|
|
81
|
+
client_id: <literal> # not a secret; see credentials below
|
|
82
|
+
client_secret_var: QB_CLIENT_SECRET
|
|
83
|
+
refresh_token_var: QB_REFRESH_TOKEN
|
|
84
|
+
realm_id: "<literal>"
|
|
85
|
+
destination:
|
|
86
|
+
database: pokehouse
|
|
87
|
+
dataset_name: quickbooks_financials
|
|
88
|
+
concurrency: 1
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**All 19 real files use that `destination: {database, dataset_name}` reference
|
|
92
|
+
form**, resolved by the `agentic-pipeline` executor at run time. `config.rs` also
|
|
93
|
+
accepts an `Inline{kind, config}` destination, but its own doc comment calls that
|
|
94
|
+
a test-fixture / post-resolution shape — no real file uses it. Field presence
|
|
95
|
+
across the 19: `name`/`source`/`destination` 19, `concurrency` 14, `description`
|
|
96
|
+
12, `schema_separator` 1, and `resources` / `allow_concurrent_runs` / `streaming`
|
|
97
|
+
/ `channel_capacity` **0** — those four are off the beaten path.
|
|
98
|
+
|
|
99
|
+
**The extension is `.airway.yml`, and it is load-bearing.** All 19 use it; none
|
|
100
|
+
uses a bare `.yml`. The compile-boundary walker
|
|
101
|
+
(`crates/oxy-compile/src/walker.rs:170`) globs literally `**/*.airway.yml` to
|
|
102
|
+
produce `FileKind::AirwayPipeline` — a `pipelines/foo.yml` is not a broken
|
|
103
|
+
pipeline, it is *invisible to compilation*, with no error to explain it.
|
|
104
|
+
|
|
105
|
+
## The wired `kind` set — the trap that costs the most time
|
|
106
|
+
|
|
107
|
+
**Oxy wires a strict subset of the engine's sources.** The upstream airway crate
|
|
108
|
+
registers roughly **40** source modules; `source_factory.rs` gives **14** of them
|
|
109
|
+
a concrete arm. YAML has no `kind` enum — `SourceConfig.kind` is a plain `String`
|
|
110
|
+
— so `kind: shopify` parses perfectly and only then fails at *run time*, in the
|
|
111
|
+
`other =>` fallback arm of the dispatch `match` inside
|
|
112
|
+
`source_factory.rs`'s `build_source_connector_inner`, with
|
|
113
|
+
``unsupported source kind `shopify`. Wire it up in
|
|
114
|
+
agentic_airway::source_factory::build_source_connector — …`` (cited by symbol,
|
|
115
|
+
not line: that arm is the *last* one in the table, so every source added above
|
|
116
|
+
it shifts it. Two did on 2026-08-17 alone — `ubereats` (#2937) and `netsuite`
|
|
117
|
+
(#2939).)
|
|
118
|
+
|
|
119
|
+
The 14 wired kinds, and how hard the real corpus exercises each:
|
|
120
|
+
|
|
121
|
+
| `kind` | Real pipelines using it |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `http_file` | 5 (census LODES + gazetteer pulls) |
|
|
124
|
+
| `quickbooks` | 5 (see below — not one pattern) |
|
|
125
|
+
| `rest_api` | 3 (`census_acs`, `nces_schools`, `yelp_fusion`) |
|
|
126
|
+
| `clickhouse` | 2 (`clickhouse_ingest`, `clickhouse_mirror`) |
|
|
127
|
+
| `besttime` | 1 |
|
|
128
|
+
| `overpass` | 1 (`osm_augmentation`) |
|
|
129
|
+
| `overture` | 1 (`overture_places`) |
|
|
130
|
+
| `toast` | 1 (`restaurant_analytics`) |
|
|
131
|
+
| `filesystem`, `sql_database`, `postgres_cdc`, `weather`, `netsuite`, `ubereats` | 0 — wired, never exercised here |
|
|
132
|
+
|
|
133
|
+
Registered upstream but **not** wired here, so unavailable today: `shopify`,
|
|
134
|
+
`stripe`, `github`, `hubspot`, `salesforce`, `slack`, `jira`, `notion`,
|
|
135
|
+
`zendesk`, `google_ads`, `google_analytics`, `google_sheets`, `facebook_ads`,
|
|
136
|
+
`airtable`, `mongodb`, `kafka`, `kinesis`, and more. **Reading airway's own
|
|
137
|
+
source list and concluding a `kind` works here is the most expensive mistake
|
|
138
|
+
this file format offers.** Check the live `match` arms instead.
|
|
139
|
+
|
|
140
|
+
**`rest_api` is the generic escape hatch.** Per `source_factory.rs`'s own doc
|
|
141
|
+
comment, the vendor-specific helpers (`shopify`, `github`, `stripe`, …) are all
|
|
142
|
+
built on `RestApiSource` upstream, so most can be expressed directly as a
|
|
143
|
+
`rest_api` config; per-vendor sugar is deferred until a real consumer asks. A
|
|
144
|
+
source with no dedicated arm is a `rest_api` config, not a blocker.
|
|
145
|
+
|
|
146
|
+
Destinations are narrower still — `destination_factory.rs` wires exactly three:
|
|
147
|
+
`memory` (test fixture), `airhouse` (production; where all 19 real pipelines
|
|
148
|
+
land), and `postgres`.
|
|
149
|
+
|
|
150
|
+
### `quickbooks` is not a normal match arm
|
|
151
|
+
|
|
152
|
+
It is dispatched by an **early return before the dispatch table**
|
|
153
|
+
(`source_factory.rs:229`), ahead of the `match` at line 236, because it is the
|
|
154
|
+
one source needing plumbing the host must supply: the `RefreshTokenSink` and
|
|
155
|
+
`AccessTokenSource` traits (`source_factory.rs:61` / `:106`), threaded in as
|
|
156
|
+
`QuickBooksTokens`. Two mutually exclusive custody modes fall out of that, and
|
|
157
|
+
`build_quickbooks` **fails closed** if a config mixes them:
|
|
158
|
+
|
|
159
|
+
- **Rotating custody** — `client_secret_var` + `refresh_token_var`: the pipeline
|
|
160
|
+
refreshes against Intuit itself and writes the new token back through the sink.
|
|
161
|
+
- **Read-only custody** — `access_token_var` alone: some *other* component owns
|
|
162
|
+
rotation, and the value is re-read per request rather than substituted once,
|
|
163
|
+
since a 60-minute token pinned at the start of a long backfill expires mid-run.
|
|
164
|
+
|
|
165
|
+
**Why that check exists, and why it matters far beyond this file:** Intuit voids
|
|
166
|
+
the old refresh token whenever it issues a new one, so two components rotating
|
|
167
|
+
the same grant deadlock into `invalid_grant` — each invalidating the other's —
|
|
168
|
+
until a human re-authorises by hand. Exactly one component may ever hold rotating
|
|
169
|
+
custody; `access_token_var` is how you keep a pipeline out of that role. The
|
|
170
|
+
source comment says it outright: a second refresher "would fork this grant's
|
|
171
|
+
rotation chain."
|
|
172
|
+
|
|
173
|
+
Hence the five real QuickBooks pipelines are **not one pattern**. The base
|
|
174
|
+
`quickbooks_financials.airway.yml` is rotating; the four per-location files
|
|
175
|
+
(`_clovis`, `_eastbay`, `_lakeoswego`, `_santarosa`) are read-only, fenced from
|
|
176
|
+
Intuit's token endpoint because a scheduled Oxy Function owns rotation. Copying
|
|
177
|
+
"the QuickBooks pipeline" as a template for a new company means first knowing
|
|
178
|
+
which custody mode that company's secret was provisioned under.
|
|
179
|
+
|
|
180
|
+
## Credentials live in the secret manager, never in the YAML
|
|
181
|
+
|
|
182
|
+
The convention is a `*_var` suffix naming a secret **key**, never a value;
|
|
183
|
+
`crates/agentic/pipeline/src/executor/mod.rs` (`resolve_airway_source_secrets`)
|
|
184
|
+
holds the per-kind `(field, var_key)` table.
|
|
185
|
+
|
|
186
|
+
- Flat configs. **The per-kind sets are disjoint — read them per row, never as
|
|
187
|
+
a shared menu:**
|
|
188
|
+
|
|
189
|
+
| `kind` | `*_var` keys it manages |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `toast` | `client_secret_var`, `client_id_var` |
|
|
192
|
+
| `quickbooks` | `client_secret_var`, `refresh_token_var` (plus `access_token_var` — see below, resolved differently) |
|
|
193
|
+
| `netsuite` | `private_key_var` |
|
|
194
|
+
| `clickhouse` | `password_var` |
|
|
195
|
+
| `weather`, `besttime` | `api_key_var` |
|
|
196
|
+
|
|
197
|
+
A `kind` with no arm in that `match` carries no managed credentials at all.
|
|
198
|
+
Borrowing across rows fails, and not gently: put `client_id_var` on a
|
|
199
|
+
quickbooks config and the executor leaves it in place (its quickbooks arm
|
|
200
|
+
strips only the two it owns), so it reaches `QuickBooksParams` — which is
|
|
201
|
+
itself `#[serde(deny_unknown_fields)]` and has no such field. Unlike the
|
|
202
|
+
top-level `deny_unknown_fields` at anatomy above, this one bites when the
|
|
203
|
+
connector is **built**, i.e. at run time: a spec that parsed cleanly dies on
|
|
204
|
+
`invalid quickbooks config: unknown field`. Same for `refresh_token_var` on a
|
|
205
|
+
toast config.
|
|
206
|
+
- **`rest_api` nests differently** — auth lives under `config.auth`, handled by
|
|
207
|
+
a separate `resolve_rest_api_auth_secrets`, as `token_var` (bearer) or
|
|
208
|
+
`key_var` (`api_key` header/query). Real, from `census_acs.airway.yml`:
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
auth:
|
|
212
|
+
type: api_key_query
|
|
213
|
+
key_var: CENSUS_API_KEY
|
|
214
|
+
param: key
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The executor resolves `*_var` → literal and then **strips the `_var` key**, so
|
|
218
|
+
the connector factory (and airway itself) only sees the resolved value under the
|
|
219
|
+
plain name — `client_secret_var` becomes `client_secret`. Hence **`password_var`
|
|
220
|
+
is not an accepted field on `ClickHouseParams`**, nor any other pair: the `_var`
|
|
221
|
+
form is valid *only* at the YAML layer.
|
|
222
|
+
|
|
223
|
+
## Run semantics
|
|
224
|
+
|
|
225
|
+
- **Partial failures are their own status.** A run that loaded some resources
|
|
226
|
+
and failed others records as **`completed_with_errors`**, not as a failure.
|
|
227
|
+
This is airway-engine behaviour Oxy inherits — it lives in the pinned engine
|
|
228
|
+
(`src/pipeline/mod.rs:743,1316,2993`, `src/airstack/mod.rs:313,352`), not in
|
|
229
|
+
the wiring crate — so a dashboard or alert that only matches "failed" reports
|
|
230
|
+
it as clean success.
|
|
231
|
+
- **Schema migration is additive-only, structurally.** `schema/evolution.rs`
|
|
232
|
+
(`diff_schemas` / `diff_tables`) emits only four changes: `TableAdded`,
|
|
233
|
+
`ColumnAdded`, `VariantColumnCreated` (a type conflict creates a *new* variant
|
|
234
|
+
column, leaving the old one alone), `WriteDispositionChanged` — no "column
|
|
235
|
+
dropped", no "type changed in place". The enum cannot represent a narrowing
|
|
236
|
+
change, so **a pipeline sitting on a wrong schema never self-heals, however
|
|
237
|
+
many times you rerun it.**
|
|
238
|
+
- **Reset schema is the explicit escape**, and a real mechanism:
|
|
239
|
+
`crates/agentic/airway/src/reset.rs` drops the pipeline's destination tables
|
|
240
|
+
and deletes its `airway_pipeline_state` row, so the next run re-infers a fresh
|
|
241
|
+
schema. Exposed as an HTTP route (`crates/agentic/http/src/routes/airway.rs`)
|
|
242
|
+
and a UI button (`web-app/src/components/airway/ResetSchemaButton.tsx`).
|
|
243
|
+
Destructive on purpose.
|
|
244
|
+
- **A retry resumes from a cursor; it does not re-pull history.** There are two
|
|
245
|
+
cursors, and confusing them wastes time. The **pipeline-global** one is
|
|
246
|
+
`airway_pipeline_state` (`state_store.rs`), keyed by `pipeline_name`: ordinary
|
|
247
|
+
incremental runs advance it, ordinary retries reuse it, and Reset is the only
|
|
248
|
+
sanctioned way to wipe it. Two runs racing that single row is half of why
|
|
249
|
+
`allow_concurrent_runs` exists — see below for the other half. The **per-run**
|
|
250
|
+
one is `airway_run_extensions.resume_state` (beside `retry_count`), used only
|
|
251
|
+
by **backfill** runs; retry there is *reset-in-place*, re-driving the same
|
|
252
|
+
`run_id` rather than cloning a new run (Toast:
|
|
253
|
+
`with_resumable_backfill_window`).
|
|
254
|
+
|
|
255
|
+
### Duplicate rows in a `replacing` table
|
|
256
|
+
|
|
257
|
+
`write_disposition: replacing` never writes the served table directly — it
|
|
258
|
+
appends to a buffer that a merge-on-read fold rebuilds latest-wins, chosen to
|
|
259
|
+
avoid the O(target) `MERGE INTO` that OOMs the data plane (the
|
|
260
|
+
`WriteDispositionLabel::Replacing` doc comment in `source_factory.rs`). **That
|
|
261
|
+
fold runs twice over: inline at the end of every successful load, and again on
|
|
262
|
+
the scheduled airhouse vacuum, from identical SQL.** Either can be the one that
|
|
263
|
+
promotes a given row, which is why the failure below is about overlap rather
|
|
264
|
+
than about the vacuum alone.
|
|
265
|
+
|
|
266
|
+
**That doc comment names the buffer `<table>_raw`, and it is wrong** — the
|
|
267
|
+
buffer is a sibling *schema*, `<schema>_raw.<table>`. So was an earlier version
|
|
268
|
+
of this skill; the correction is not a version bump, since the migration
|
|
269
|
+
predates every airway rev oxy has pinned, including 0.1.30. `oxy-data-architecture`
|
|
270
|
+
carries the location, the stale comments, and where the buffer sits in the wider
|
|
271
|
+
picture. That shape is where duplicates come from:
|
|
272
|
+
each run ends with a merge-on-read fold, and **two folds whose snapshots overlap
|
|
273
|
+
each purge against a base the other has not committed yet, so both versions of a
|
|
274
|
+
changed row survive** (`extension/migration.rs:432`). Measured, not
|
|
275
|
+
hypothetical — pokehouse, 2026-08-05: 34 excess rows in `toast_pos.orders`, 104
|
|
276
|
+
in `order_selections`, **every duplicate pair spanning two `_aw_load_id`s**
|
|
277
|
+
(`migration.rs:435`). That span is the diagnostic, so check it *before* auditing
|
|
278
|
+
your own config: a pair straddling two load ids is this bug, not yours — and if
|
|
279
|
+
it doesn't straddle, this section does not explain your duplicates.
|
|
280
|
+
|
|
281
|
+
The guard is the single-flight lease `airway_pipeline_leases` — at most one
|
|
282
|
+
active run per `(workspace_id, pipeline_name)`, taken by one atomic
|
|
283
|
+
`INSERT … ON CONFLICT DO UPDATE … WHERE expires_at < now()` so the database
|
|
284
|
+
resolves the race rather than application code (`extension/pipeline_lease.rs:1`,
|
|
285
|
+
`:16`). A process-local mutex was rejected explicitly: Oxy runs multiple
|
|
286
|
+
`oxy-serve`/`oxy-worker` replicas, and the engine's own `COMPACTION_GATE` rests
|
|
287
|
+
on a single-writer assumption that stopped holding once the data plane scaled
|
|
288
|
+
past one replica (`pipeline_lease.rs:7`). Two consequences follow.
|
|
289
|
+
**`allow_concurrent_runs: true` turns the guard off** — `config.rs:44` says to
|
|
290
|
+
set it only on a pipeline owning no cursor and writing no `replacing` table,
|
|
291
|
+
since on a `replacing` pipeline it re-admits exactly this bug. And **a
|
|
292
|
+
dead-lettered run never releases its lease**: the 6-hour `LEASE_TTL_SECS` is a
|
|
293
|
+
crash backstop, not a reclaim policy, and the generic reaper cannot touch the
|
|
294
|
+
table without leaking a domain table into `agentic-runtime`
|
|
295
|
+
(`pipeline_lease.rs:73`, `:104`) — so a pipeline stuck "busy" with nothing
|
|
296
|
+
running is that gap, and wants the manual release, not a six-hour wait.
|
|
297
|
+
|
|
298
|
+
The lease stops *new* overlapping folds; being an admission guard, it cannot
|
|
299
|
+
repair rows that already landed. Those need Reset schema.
|
|
300
|
+
|
|
301
|
+
## Traps
|
|
302
|
+
|
|
303
|
+
**The `$schema` header several real pipelines carry is dead, and its failure is
|
|
304
|
+
silent.** Four of the 19 (`quickbooks_financials_clovis`, `_eastbay`,
|
|
305
|
+
`_lakeoswego`, `_santarosa`) open with a `# yaml-language-server: $schema=`
|
|
306
|
+
comment pointing at
|
|
307
|
+
`raw.githubusercontent.com/oxy-hq/oxy/refs/heads/main/json-schemas/pipeline.json`
|
|
308
|
+
(scheme omitted deliberately — the link is dead and this repo's link check fails
|
|
309
|
+
any non-200). Curled: **404**. `oxy-hq/oxy` was renamed to `oxy-hq/oxygen` and
|
|
310
|
+
the path 404s there too; that repo's `json-schemas/` holds 8 files and **no
|
|
311
|
+
`pipeline.json` — no schema for `.airway.yml` exists at all.** Start from a real
|
|
312
|
+
pipeline and you inherit an editor integration that validates nothing, warns
|
|
313
|
+
about nothing, and looks like it works. Delete the line. (The other 15 carry no
|
|
314
|
+
pointer at all, so IDE validation is inconsistent across the corpus even before
|
|
315
|
+
the link being dead.)
|
|
316
|
+
|
|
317
|
+
**Omitting `concurrency` means `1`, not "unlimited".** The default is forced
|
|
318
|
+
sequential and nothing announces it: the 5 files that omit it (the census
|
|
319
|
+
`http_file` pulls) run serially, while the 14 that set it use 2–4. A pipeline
|
|
320
|
+
that just forgets the key is not misconfigured — it is quietly slow.
|
|
321
|
+
|
|
322
|
+
**Two `clickhouse` source footguns, both silent.** (a) `ClickHouseParams`' own
|
|
323
|
+
doc comment admits that `port: 8443` with no `secure:` "silently uses plaintext
|
|
324
|
+
on a TLS port… left as-is deliberately" — tolerable only because the wizard emits
|
|
325
|
+
both keys together, and these are hand-edited files. State `secure:` explicitly
|
|
326
|
+
and match it to the port, as `clickhouse_mirror.airway.yml` does (`8123`/`false`).
|
|
327
|
+
(b) Socket timeouts belong in `settings:`, never a SQL `SETTINGS` clause:
|
|
328
|
+
ClickHouse fixes the response socket's timeout at request setup, *before* parsing
|
|
329
|
+
the query body, so an inline `SETTINGS http_send_timeout=…` is accepted and
|
|
330
|
+
ignored — exactly what someone reaches for when tuning a slow mirror.
|
|
331
|
+
|
|
332
|
+
**A `kind` claim is only as good as the pin it was checked against, and in-tree
|
|
333
|
+
comments do not self-update.** `overture_places.airway.yml` still carries a
|
|
334
|
+
header saying the connector has not landed and the airway rev must be pinned to
|
|
335
|
+
a build registering `kind: overture` — which at 0.1.30 it does, so the comment
|
|
336
|
+
has been stale for a while with nothing to flag it. Read that as the standing
|
|
337
|
+
warning about this whole document: verify against `source_factory.rs` at the rev
|
|
338
|
+
you are running.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oxy-automations
|
|
3
|
+
description: Use when writing, debugging, or reviewing an Oxy automation (a `.automation.yml` / legacy `.procedure.yml` workflow — tasks, loops, variables, schedules) in a customer workspace, or when an automation *run* misbehaves: an `http_request` task that works locally and fails in cloud, a file that is silently not an automation, or an automation run landing `completed_with_errors`. Covers the file-naming trap, task-type gotchas, and where the authoritative reference lives.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Oxy automations
|
|
7
|
+
|
|
8
|
+
**The reference is upstream and maintained — read it rather than guessing:**
|
|
9
|
+
<https://www.oxygen-hq.com/docs/guide/build/automations>
|
|
10
|
+
(`index`, `loops`, `task-types`, `variables`).
|
|
11
|
+
|
|
12
|
+
This skill deliberately does **not** restate the schema. It carries the things
|
|
13
|
+
that reference does not tell you, and the mistakes that cost time.
|
|
14
|
+
|
|
15
|
+
## The file-naming trap
|
|
16
|
+
|
|
17
|
+
The canonical extension is **`.automation.yml`**.
|
|
18
|
+
|
|
19
|
+
- `.procedure.yml` is the **legacy** name. It is still accepted, so a workspace
|
|
20
|
+
full of it keeps working and nothing warns you — which is exactly why it
|
|
21
|
+
spreads. Poke House's workspace has 18 automations, and every one of them is
|
|
22
|
+
still `.procedure.yml`.
|
|
23
|
+
- `.workflow.yml` is **no longer a recognised file kind at all**. A file with
|
|
24
|
+
that name is silently not an automation.
|
|
25
|
+
- `oxy migrate-automations` exists to rename legacy files. It is the only thing
|
|
26
|
+
that reads them for that purpose.
|
|
27
|
+
|
|
28
|
+
**Write new files as `.automation.yml`.** Do not copy the extension from a
|
|
29
|
+
neighbouring file in the workspace you are editing — that is how the legacy name
|
|
30
|
+
propagated in the first place.
|
|
31
|
+
|
|
32
|
+
## Naming, and why the code disagrees with the docs
|
|
33
|
+
|
|
34
|
+
"Automation" is the current name for what used to be called a Procedure, and
|
|
35
|
+
before that a Workflow. The Rust types still say `Workflow*` / `Procedure*`, and
|
|
36
|
+
`type: workflow` plus the `agentic_workflow_state` table are **wire and storage
|
|
37
|
+
contracts** — they are not stale naming to be tidied up. Seeing `workflow` in the
|
|
38
|
+
code or in a payload does not mean you are looking at something deprecated.
|
|
39
|
+
|
|
40
|
+
Routes follow the same split: `/automations/:id` is canonical;
|
|
41
|
+
`/workflows/:id`, `/procedures`, and `/agentic-workflows` are aliases.
|
|
42
|
+
|
|
43
|
+
## Task-type gotchas
|
|
44
|
+
|
|
45
|
+
- **`http_request` is HTTPS-only and blocks private egress.** Requests to
|
|
46
|
+
localhost, cloud-metadata endpoints, and private IP ranges are refused unless
|
|
47
|
+
that task opts in with `allow_hosts`. A task that works locally and fails in
|
|
48
|
+
cloud is usually this, not a network problem.
|
|
49
|
+
- **Partial failures are their own state.** A run where some steps failed records
|
|
50
|
+
as `completed_with_errors`, not as a failed run. Code and dashboards that only
|
|
51
|
+
check for "failed" will report it as a success.
|
|
52
|
+
|
|
53
|
+
## Before you write one
|
|
54
|
+
|
|
55
|
+
Check whether the work belongs in an automation at all. Scheduled data movement
|
|
56
|
+
is usually an Airway pipeline (`.airway.yml`); a question answered from the
|
|
57
|
+
semantic layer is usually an agent or a Data App. Automations are for
|
|
58
|
+
multi-step orchestration you want to run on a schedule or trigger.
|
|
59
|
+
|
|
60
|
+
**But "usually a pipeline" is not "never an automation", and the seam is
|
|
61
|
+
undocumented.** There is a `type: airway` task that runs a pipeline as a step —
|
|
62
|
+
absent from the task-types page — and a step completes only once the pipeline's
|
|
63
|
+
end-of-load fold has committed. That is what makes the standard
|
|
64
|
+
ingest-then-aggregate automation safe: `type: airway` followed by
|
|
65
|
+
`type: execute_sql` building a rollup table. Reach for it whenever a dashboard
|
|
66
|
+
would otherwise aggregate at query time. **`oxy-data-architecture`** owns that
|
|
67
|
+
pattern and the guarantee behind it.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oxy-cli
|
|
3
|
+
description: Use when you need data out of an Oxy deployment — querying a customer's warehouse or semantic layer, reading threads/runs/apps/orgs, checking why an endpoint 4xxs, or reproducing a customer-reported bug against real data. Also when you need to know which endpoints exist or what body one takes. Triggers on "query the customer's data", "what does this API return", "debug this in production/dev", "which endpoint", "check the thread/run/app", "oxyc", "oxy api".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Getting data out of Oxy with `oxyc`
|
|
7
|
+
|
|
8
|
+
`oxyc` is an authenticated `gh api`-shaped client. Prefer it over `curl` — it
|
|
9
|
+
resolves the token, picks the right credential for the surface, and can tell
|
|
10
|
+
you what endpoints exist.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Not published to npm yet, so `npx @oxy-hq/cli` 404s. From a checkout:
|
|
14
|
+
node <repo>/sdk/cli/dist/main.mjs <command>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## The loop that works
|
|
18
|
+
|
|
19
|
+
**Never guess a path.** Three commands, in this order:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
oxyc routes sql # 1. what exists, and what it does
|
|
23
|
+
oxyc schema {workspace}/sql/query # 2. the body it expects
|
|
24
|
+
oxyc api {workspace}/sql/query \
|
|
25
|
+
-f 'sql=select 1' -f database=<name> --md # 3. call it
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
To get data out of a customer's warehouse, that is the whole path:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
oxyc api orgs --md # org ids
|
|
32
|
+
oxyc api {org}/workspaces --md # workspace ids
|
|
33
|
+
oxyc api {workspace}/databases --jq '.[].name' # connection names
|
|
34
|
+
oxyc api {workspace}/sql/query -f 'sql=…' -f database=… --md
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`oxyc routes <filter>` matches on method, path, surface and description, so
|
|
38
|
+
`oxyc routes query`, `oxyc routes admin`, `oxyc routes semantic` all work. Run
|
|
39
|
+
it with no filter only when you genuinely want all ~670 — it is a lot of
|
|
40
|
+
context.
|
|
41
|
+
|
|
42
|
+
## Placeholders — do not hunt for ids
|
|
43
|
+
|
|
44
|
+
`{org}`, `{workspace}`, `{project}`, `{customer}` and `{me}` fill themselves
|
|
45
|
+
from the customer repo you are standing in, or from `--org` / `--workspace` /
|
|
46
|
+
`--project`.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
oxyc api {org}/workspaces --md # find a workspace id
|
|
50
|
+
oxyc api {workspace}/agents
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
An unresolved placeholder errors and names the flag that would fill it. It is
|
|
54
|
+
never sent to the server as a literal.
|
|
55
|
+
|
|
56
|
+
## Keep the context small
|
|
57
|
+
|
|
58
|
+
- `--jq '<expr>'` on the server-side shape, **always**, before you look at a
|
|
59
|
+
large response. Dumping a full thread list into context is the common waste.
|
|
60
|
+
- `--md` turns an array of objects into a markdown table — far fewer tokens
|
|
61
|
+
than the same rows as JSON, which repeats every field name per row.
|
|
62
|
+
- `--cache 5m` when you are walking the same endpoints repeatedly.
|
|
63
|
+
- `--paginate` merges every page into one document. It is a heuristic on this
|
|
64
|
+
API (pagination is not uniform); if a result looks short, pass
|
|
65
|
+
`--paginate-key <field>`.
|
|
66
|
+
|
|
67
|
+
## Beyond the API
|
|
68
|
+
|
|
69
|
+
oxyc validate # check the workspace YAML — no network, no token
|
|
70
|
+
oxyc proxy --env dev # local app dev against cloud data
|
|
71
|
+
oxyc guide # this page, to paste into a context file
|
|
72
|
+
|
|
73
|
+
`oxyc validate` is the one command that works entirely offline. It checks
|
|
74
|
+
`config.yml`, `.automation.yml`, `.agentic.yml`, `.app.yml` and
|
|
75
|
+
`.agent.test.yml` against the schemas the Rust config types generate. It is
|
|
76
|
+
STRUCTURAL only — `oxy validate` also resolves `databases:` and `llm.ref`, and
|
|
77
|
+
wins where the two disagree.
|
|
78
|
+
|
|
79
|
+
If your runtime speaks MCP, `oxyc mcp` serves the same API surface as four
|
|
80
|
+
tools (`oxy_routes`, `oxy_schema`, `oxy_request`, `oxy_whoami`) instead.
|
|
81
|
+
|
|
82
|
+
## Branch on the exit code, not the text
|
|
83
|
+
|
|
84
|
+
| | |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `0` | fine |
|
|
87
|
+
| `2` | you called it wrong — fix the command, do not retry |
|
|
88
|
+
| `4` | not authenticated → `oxyc login --env <env>`, or the token expired |
|
|
89
|
+
| `5` | 404 — check the path with `oxyc routes`. In an **admin** surface a 404 can be a scope boundary, not a missing row. |
|
|
90
|
+
| `6` | the request was malformed — check `oxyc schema` |
|
|
91
|
+
| `7` | 5xx / timeout / network — **retryable** |
|
|
92
|
+
| `8` | refused: the operation would have destroyed something |
|
|
93
|
+
|
|
94
|
+
## Traps specific to this API
|
|
95
|
+
|
|
96
|
+
- **`200` with a body of `null` can mean an expired session**, not "no such
|
|
97
|
+
thing" — `/api/user` does exactly that. `oxyc` warns on stderr when it sees
|
|
98
|
+
one; `oxyc whoami` tells the two apart.
|
|
99
|
+
- **`oxyc schema` covers the data plane, not everything.** The document is
|
|
100
|
+
curated for exactly the endpoints you need to build a body for — SQL, semantic
|
|
101
|
+
query, and the lookups that resolve ids. A blank schema means *undocumented*,
|
|
102
|
+
not nonexistent: `oxyc routes <path>` confirms the endpoint is real and shows
|
|
103
|
+
what the handler says it does.
|
|
104
|
+
- **`/sql/query` does not return an object by default.** It returns arrays of
|
|
105
|
+
strings, **header row first** — `[["id","name"],["1","ada"]]`. `--md` renders
|
|
106
|
+
that as a table; `--jq '.[1:]'` skips the header. Only `result_format:
|
|
107
|
+
"parquet"` returns an object, and only that one carries `truncated`.
|
|
108
|
+
- **A listed route can still 404** if it is `ide-only` or `worker-only`; those
|
|
109
|
+
are hidden by default and shown by `oxyc routes --all`.
|
|
110
|
+
- **Pick the environment deliberately.** `--env local|dev|staging|production`
|
|
111
|
+
(default production), or paste a URL: `--env https://poke-house.oxygen-hq.com`
|
|
112
|
+
targets that deployment *and* sets `{org}`.
|
|
113
|
+
- **Staff hitting a tenant surface need an assume-role session.** A 403 there
|
|
114
|
+
usually means no active session, not a mis-modeled role — check with
|
|
115
|
+
`oxyc assume status`, which answers "none" without failing.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
oxyc assume start --org <slug|uuid|url> -r "why" # 60 min, not renewable
|
|
119
|
+
oxyc assume status # what is live, minutes left
|
|
120
|
+
oxyc assume end # or --all
|
|
121
|
+
oxyc login --login-env dev,staging # log into several at once
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The session hangs off your **account**, not this terminal — your browser is in
|
|
125
|
+
there too, and `end` gets you out of both.
|
|
126
|
+
|
|
127
|
+
## Do not
|
|
128
|
+
|
|
129
|
+
- Do not `curl` the API by hand — you will get the credential wrong for
|
|
130
|
+
`/external/api/**`, which takes `X-API-Key` rather than a bearer.
|
|
131
|
+
- Do not run a mutating request (`-X POST/PATCH/DELETE`) against **production**
|
|
132
|
+
on your own initiative. Read freely; ask before you write.
|
|
133
|
+
- Do not paste a token into a command line or a file. `oxyc` reads it from the
|
|
134
|
+
login cache; `oxyc token` exists if something really needs it.
|