okengine 0.19.3 → 0.19.4

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 (47) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/channel/index.mdx +1 -1
  3. package/site/content/docs/elements/clock/index.mdx +53 -105
  4. package/site/content/docs/elements/clock/schedules.mdx +39 -11
  5. package/site/content/docs/elements/flow/consumers.mdx +54 -31
  6. package/site/content/docs/elements/flow/index.mdx +3 -3
  7. package/site/content/docs/elements/flow/routing.mdx +11 -15
  8. package/site/content/docs/elements/signal/broadcast.mdx +6 -1
  9. package/site/content/docs/elements/signal/index.mdx +21 -177
  10. package/site/content/docs/elements/signal/once.mdx +6 -1
  11. package/site/content/docs/reference/errors.mdx +7 -0
  12. package/site/content/docs/understand/the-architecture.mdx +2 -2
  13. package/src/cli/competitor-mention-removal.test.ts +3 -3
  14. package/src/compiler/extract.test.ts +190 -15
  15. package/src/compiler/extract.ts +47 -4
  16. package/src/compiler/flow-path.test.ts +23 -0
  17. package/src/compiler/flow-path.ts +17 -1
  18. package/src/console/ui-next/dist/assets/{access-page-BpugjHHY.js → access-page-rO2HLPab.js} +1 -1
  19. package/src/console/ui-next/dist/assets/{agent-disclosure-qnsmtJhf.js → agent-disclosure-ae8w8JLx.js} +1 -1
  20. package/src/console/ui-next/dist/assets/{cache-glyph-BcpWUM98.js → cache-glyph-C-uHdh5d.js} +1 -1
  21. package/src/console/ui-next/dist/assets/{call-pii-button-Bz5xiNmR.js → call-pii-button-BJH54w4s.js} +1 -1
  22. package/src/console/ui-next/dist/assets/{collapsible-Cuxn2WH8.js → collapsible-CaE8cs9p.js} +1 -1
  23. package/src/console/ui-next/dist/assets/{duration-tone-Bjnl3EaM.js → duration-tone-DdOQkQVK.js} +1 -1
  24. package/src/console/ui-next/dist/assets/{flows-page-DVujp-T2.js → flows-page-DWeBszQQ.js} +1 -1
  25. package/src/console/ui-next/dist/assets/{highlighted-json-Bql0qlqW.js → highlighted-json-B2gBsC4B.js} +1 -1
  26. package/src/console/ui-next/dist/assets/{http-method-BljvrfRg.js → http-method-BnMl0dJt.js} +1 -1
  27. package/src/console/ui-next/dist/assets/{index-D1vE656k.js → index-CBAP48v5.js} +3 -3
  28. package/src/console/ui-next/dist/assets/index-VxoEz295.css +2 -0
  29. package/src/console/ui-next/dist/assets/{observability-page-Bdi0bLJl.js → observability-page-DVpcJEnW.js} +1 -1
  30. package/src/console/ui-next/dist/assets/{replica-lag-CpkPfITG.js → replica-lag-BUQf2Nf6.js} +1 -1
  31. package/src/console/ui-next/dist/assets/{request-meta-C4ZVNFVt.js → request-meta-CPkGDQgT.js} +1 -1
  32. package/src/console/ui-next/dist/assets/{store-page-DAHtnesC.js → store-page-CwS_cY4V.js} +1 -1
  33. package/src/console/ui-next/dist/assets/{trace-detail-sheet-B2c9QRBw.js → trace-detail-sheet-DNX7Y2bd.js} +1 -1
  34. package/src/console/ui-next/dist/assets/{tree-expand-toggle-BP4tRC98.js → tree-expand-toggle-Bhdk27aF.js} +1 -1
  35. package/src/console/ui-next/dist/assets/{units-page-Oi6--n09.js → units-page-CsBh0XbI.js} +1 -1
  36. package/src/console/ui-next/dist/assets/{vault-page-Dt-cPkiU.js → vault-page-CsTlPtK9.js} +1 -1
  37. package/src/console/ui-next/dist/index.html +2 -2
  38. package/src/kernel/app.ts +23 -1
  39. package/src/kernel/errors-flow-name.ts +20 -4
  40. package/src/kernel/errors.registry.test.ts +4 -2
  41. package/src/kernel/flow-name.test.ts +105 -20
  42. package/src/kernel/flow.ts +3 -3
  43. package/src/kernel/on.ts +0 -5
  44. package/src/kernel/stamp-http.test.ts +2 -1
  45. package/src/kernel/stamp-http.ts +5 -7
  46. package/src/kernel/unit.ts +1 -2
  47. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.19.3",
3
+ "version": "0.19.4",
4
4
  "description": "One law. Eight elements. One contract. The backend model stays small; operational surfaces are derived from it instead of maintained separately.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -57,7 +57,7 @@ import { noteCreated } from "./signals";
57
57
 
58
58
  export const onCreated = on(
59
59
  noteCreated,
60
- flow({
60
+ flow("notes.onCreated", {
61
61
  do: async (payload, fx) => {
62
62
  await fx.send(noteCreatedMail, {
63
63
  to: "you@localhost",
@@ -5,7 +5,7 @@ icon: "Clock"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- Clock is how your backend **knows what time it is and when to run again**. A monthly invoice job, a 30s health ping, and a three-day trial reminder share one vocabulary: declare a named schedule, bind it with `on(clockDecl, flow)`, and read time only through `fx.clock`.
8
+ Clock is how your backend **knows what time it is and when to run again**. A monthly invoice job, a 30s health ping, and a three-day trial reminder share one vocabulary: declare a named schedule, bind it with `on(clockDecl, flow("name", { do }))`, and read time only through `fx.clock`.
9
9
 
10
10
  For developers scheduling work on okengine — one handle shape; drivers swap by environment.
11
11
 
@@ -58,9 +58,8 @@ export const digest = on(
58
58
  ### See it tick
59
59
 
60
60
  With `oke dev`, the scheduler reconciles `notes.digest` into the Store and leader-elects
61
- before each fire. `do` receives no payload — read time with `fx.clock.now()` (epoch-ms for
62
- math). Map to ISO on the wire when the Flow returns a human-facing instant
63
- (`new Date(fx.clock.now()).toISOString()`).
61
+ before each fire. `do` receives no payload — read time with `fx.clock.now()` (epoch-ms).
62
+ Map to ISO on the wire with `new Date(fx.clock.now()).toISOString()`.
64
63
 
65
64
  Under `drivers.clock.test = "frozen"`, advance time in tests instead of waiting a day.
66
65
 
@@ -69,35 +68,39 @@ Under `drivers.clock.test = "frozen"`, advance time in tests instead of waiting
69
68
  </Steps>
70
69
 
71
70
  <Callout title="Jobs are Flows">
72
- `on(clockDecl, flow)` is the same species as Signal consumers and HTTP handlers. There is no
73
- separate job runner — see [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs).
71
+ Write the schedule inline or export it — [Inline or named export](#inline-or-named-export).
72
+ Every consumer still uses `flow("name", { do })`. See
73
+ [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs).
74
74
  </Callout>
75
75
 
76
76
  ## Inline or named export
77
77
 
78
- | Style | When |
79
- | --------------------------------------------- | ----------------------------------------------------------------- |
80
- | `on(clock.every("name", "1h"), flow({ do }))` | Self-contained — nothing else needs the Clock handle |
81
- | `export const x = clock.every("name", "1h")` | Another file (or a second `on()`) must reuse the same declaration |
78
+ | Style | When |
79
+ | --------------------------------------------------------- | --------------------------------- |
80
+ | `on(clock.every("name", "1h"), flow("ops.ping", { do }))` | One Flow owns this schedule |
81
+ | `export const x = clock.every("name", "1h")` | Several Flows share this schedule |
82
82
 
83
- Both styles stamp the same Manifest `flow.trigger`. The choice is where the
84
- declaration lives, not two runtimes.
83
+ <Callout title="Why Clock can be inline">
84
+ `fx.clock` is now / sleep / offsets — not an emit. Signal stays an exported const so producers can
85
+ `fx.emit(handle, payload)`. Export a Clock const only to share one schedule across named Flows.
86
+ </Callout>
85
87
 
86
- What the Flow is called is a separate choice — [Flow name](#flow-name).
88
+ Both styles pass a real `flow("name", { do })`. Nameless `flow({ do })` fails
89
+ **OKE1072** — the trigger's name is never the Flow's name.
87
90
 
88
91
  <Tabs items={["Inline", "Named"]}>
89
92
 
90
93
  <Tab value="Inline">
91
94
 
92
- One file — declare and bind together. The scheduler fires it; no other file
93
- imports the handle:
95
+ One file — declare and bind together. The scheduler fires it; no other Flow
96
+ binds this cadence:
94
97
 
95
98
  ```typescript title="src/flows/health/ping.ts"
96
99
  import { on, flow, clock } from "okengine";
97
100
 
98
101
  export const pingExternal = on(
99
102
  clock.every("health.pingExternal", "30s"),
100
- flow({
103
+ flow("health.pingExternal", {
101
104
  plane: "operator",
102
105
  do: async (_, fx) => {
103
106
  await fx.call(pingUpstream);
@@ -110,79 +113,21 @@ export const pingExternal = on(
110
113
 
111
114
  <Tab value="Named">
112
115
 
113
- Export the handle when another file (or a second `on()`) must reuse the same
114
- declaration:
116
+ Export the handle when a second `on()` must reuse the same schedule. Each Flow
117
+ keeps its own explicit name — **OKE1070** if they collide:
115
118
 
116
- ```typescript title="src/clocks/digest.ts"
119
+ ```typescript title="src/clocks/metrics.ts"
117
120
  import { clock } from "okengine";
118
121
 
119
- export const digestClock = clock.every("notes.digest", "1d");
122
+ export const tickClock = clock.every("metrics.tick", "1h");
120
123
  ```
121
124
 
122
- ```typescript title="src/flows/notes/digest.ts"
125
+ ```typescript title="src/flows/ops/metrics.ts"
123
126
  import { on, flow } from "okengine";
124
- import { digestClock } from "@/clocks/digest";
125
-
126
- export const digest = on(
127
- digestClock,
128
- flow("notes.digest", {
129
- plane: "operator",
130
- do: async (_, fx) => {
131
- await fx.call(buildDigest, { at: fx.clock.now() });
132
- },
133
- }),
134
- );
135
- ```
136
-
137
- </Tab>
138
-
139
- </Tabs>
140
-
141
- ## Flow name
142
-
143
- | Style | When |
144
- | ------------------------- | ---------------------------------------------------------------- |
145
- | `flow({ do })` | No unit folder — Flow name is the Clock name (`metrics.cleanup`) |
146
- | `flow("ops.sweep")` | Manifest / `fx.call` name must differ from the Clock |
147
- | Tree `export const sweep` | `src/flows/ops/sweep.ts` stamps `ops.sweep` — overwrites inherit |
148
-
149
- Explicit `flow("…")` and the file tree overwrite inherit. HTTP does not inherit a
150
- name from the path — nameless HTTP stays for the tree or fails **OKE1045**. Two
151
- Flows that land on the same name fail **OKE1070** (`Flow "{flow}" is defined twice.`).
152
-
153
- <Tabs items={["Inherit", "Explicit", "Tree"]}>
154
-
155
- <Tab value="Inherit">
156
-
157
- A file directly in `src/flows/` (no unit folder) has nothing to stamp. The Flow
158
- is named `metrics.cleanup` — same as the Clock:
159
-
160
- ```typescript title="src/flows/cleanup.ts"
161
- import { on, flow, clock } from "okengine";
162
-
163
- export const cleanupMetrics = on(
164
- clock.every("metrics.cleanup", "1h"),
165
- flow({
166
- plane: "operator",
167
- do: async (_, fx) => {
168
- await fx.call(sweepMetrics);
169
- },
170
- }),
171
- );
172
- ```
173
-
174
- </Tab>
175
-
176
- <Tab value="Explicit">
177
-
178
- The Clock stays `metrics.cleanup`. The Flow is `ops.sweep` — that is the
179
- Manifest / `fx.call` name:
180
-
181
- ```typescript title="src/flows/sweep.ts"
182
- import { on, flow, clock } from "okengine";
127
+ import { tickClock } from "@/clocks/metrics";
183
128
 
184
129
  export const sweep = on(
185
- clock.every("metrics.cleanup", "1h"),
130
+ tickClock,
186
131
  flow("ops.sweep", {
187
132
  plane: "operator",
188
133
  do: async (_, fx) => {
@@ -190,24 +135,13 @@ export const sweep = on(
190
135
  },
191
136
  }),
192
137
  );
193
- ```
194
-
195
- </Tab>
196
-
197
- <Tab value="Tree">
198
-
199
- Unit folder + `export const` stamps `unit.export`. The Flow is `ops.sweep`, not
200
- the Clock name `cleanup`:
201
138
 
202
- ```typescript title="src/flows/ops/sweep.ts"
203
- import { on, flow, clock } from "okengine";
204
-
205
- export const sweep = on(
206
- clock.every("cleanup", "10m"),
207
- flow({
139
+ export const report = on(
140
+ tickClock,
141
+ flow("ops.report", {
208
142
  plane: "operator",
209
143
  do: async (_, fx) => {
210
- await fx.call(sweepMetrics);
144
+ await fx.call(reportMetrics);
211
145
  },
212
146
  }),
213
147
  );
@@ -227,12 +161,17 @@ Same `clock` + `fx.clock` from a calendar cron to an interval, a durable pause,
227
161
 
228
162
  Five-field cron plus helpers. Prefer an app-wide zone so schedules stay short:
229
163
 
230
- ```typescript title="src/flows/reports/daily.ts"
231
- import { on, flow, clock } from "okengine";
164
+ ```typescript title="src/clocks/reports.ts"
165
+ import { clock } from "okengine";
232
166
 
233
167
  export const dailyReportClock = clock.daily("reports.daily", {
234
168
  at: "06:00",
235
169
  });
170
+ ```
171
+
172
+ ```typescript title="src/flows/reports/daily.ts"
173
+ import { on, flow } from "okengine";
174
+ import { dailyReportClock } from "@/clocks/reports";
236
175
 
237
176
  export const runDailyReport = on(
238
177
  dailyReportClock,
@@ -255,10 +194,15 @@ see [Schedules](/docs/elements/clock/schedules).
255
194
 
256
195
  Human durations — integer + unit, no weeks: `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"`:
257
196
 
258
- ```typescript title="src/flows/health/ping.ts"
259
- import { on, flow, clock } from "okengine";
197
+ ```typescript title="src/clocks/health.ts"
198
+ import { clock } from "okengine";
260
199
 
261
200
  export const pingClock = clock.every("health.pingExternal", "30s");
201
+ ```
202
+
203
+ ```typescript title="src/flows/health/ping.ts"
204
+ import { on, flow } from "okengine";
205
+ import { pingClock } from "@/clocks/health";
262
206
 
263
207
  export const pingExternal = on(
264
208
  pingClock,
@@ -414,9 +358,14 @@ export default defineConfig({
414
358
  </Accordion>
415
359
 
416
360
  <Accordion title="OKE1070 — flow name defined twice">
417
- Cause: `Flow "{flow}" is defined twice.` Two nameless consumers inherited the same Clock name, or
418
- two explicit `flow("…")` calls collide. Give at least one a distinct `flow("…")` or tree export —
419
- [Flow name](#flow-name).
361
+ Cause: `Flow "{flow}" is defined twice.` Two `flow("…")` strings collide. Give at least one a
362
+ distinct name. Clock allows several consumers on one schedule — each needs its own name.
363
+ </Accordion>
364
+
365
+ <Accordion title="OKE1072 — Clock flow unnamed">
366
+ Cause: `A clock flow on "{trigger}" has no name.`
367
+ Fix: pass an explicit name — `on(clockDecl, flow("notes.digest", { do }))`.
368
+ Inline `clock.every` still needs `flow("…")` — [Inline or named export](#inline-or-named-export).
420
369
  </Accordion>
421
370
 
422
371
  </Accordions>
@@ -428,8 +377,7 @@ export default defineConfig({
428
377
  - [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs) — bind with `on(clockDecl, flow)`
429
378
  - [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` around sleeps
430
379
  - [fx](/docs/reference/fx) — full `fx.clock` table
431
- - [Routing](/docs/elements/flow/routing#names) — tree `unit.export` vs inherit
432
- - [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError` · OKE1070
380
+ - [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError` · OKE1070 · OKE1072
433
381
 
434
382
  ## Next
435
383
 
@@ -25,10 +25,15 @@ For developers running daily digests, health pings, and per-tenant billing loops
25
25
  <Step>
26
26
  ### Declare a schedule and bind a Flow
27
27
 
28
- ```typescript title="src/flows/reports/daily.ts"
29
- import { on, flow, clock } from "okengine";
28
+ ```typescript title="src/clocks/reports.ts"
29
+ import { clock } from "okengine";
30
30
 
31
31
  export const dailyReportClock = clock.daily("reports.daily", { at: "06:00" });
32
+ ```
33
+
34
+ ```typescript title="src/flows/reports/daily.ts"
35
+ import { on, flow } from "okengine";
36
+ import { dailyReportClock } from "@/clocks/reports";
32
37
 
33
38
  export const runDailyReport = on(
34
39
  dailyReportClock,
@@ -112,10 +117,15 @@ chosen programmatically: `clock("x", { cron: "0 6 * * *" })`, Bun nicknames
112
117
 
113
118
  Fixed duration loops — `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"`:
114
119
 
115
- ```typescript title="src/flows/health/ping.ts"
116
- import { on, flow, clock } from "okengine";
120
+ ```typescript title="src/clocks/health.ts"
121
+ import { clock } from "okengine";
117
122
 
118
123
  export const pingClock = clock.every("health.pingExternal", "30s");
124
+ ```
125
+
126
+ ```typescript title="src/flows/health/ping.ts"
127
+ import { on, flow } from "okengine";
128
+ import { pingClock } from "@/clocks/health";
119
129
 
120
130
  export const pingExternal = on(
121
131
  pingClock,
@@ -146,9 +156,14 @@ Unknown duration strings parse as `0` ms and never become due. A `"d"` is exactl
146
156
  template name is never ticked:
147
157
 
148
158
  ```typescript title="src/clocks/invoices.ts"
149
- import { on, flow, clock } from "okengine";
159
+ import { clock } from "okengine";
150
160
 
151
161
  export const invoicesClock = clock.perTenant("invoices", { every: "1h" });
162
+ ```
163
+
164
+ ```typescript title="src/flows/billing/invoices.ts"
165
+ import { on, flow } from "okengine";
166
+ import { invoicesClock } from "@/clocks/invoices";
152
167
 
153
168
  export const runInvoices = on(
154
169
  invoicesClock,
@@ -275,16 +290,22 @@ do not consult the zone for tick spacing; the zone still lands on the Store row.
275
290
 
276
291
  ## Binding & Input
277
292
 
278
- Bind the returned handle with `on(clockDecl, flow)`. The scheduler invokes `do` with **no
279
- payload** — destructure `_` and read time through `fx.clock`:
293
+ Bind with `on(clockDecl, flow("name", { do }))`. One Flow can write `clock.every`
294
+ inside `on()` — [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
295
+ `do` has no payload; read time through `fx.clock`:
296
+
297
+ ```typescript title="src/clocks/metrics.ts"
298
+ import { clock } from "okengine";
299
+
300
+ export const cleanupClock = clock.every("metrics.cleanup", "1h");
301
+ ```
280
302
 
281
303
  ```typescript title="src/flows/metrics/cleanup.ts"
282
- import { on, flow, clock } from "okengine";
304
+ import { on, flow } from "okengine";
283
305
  import { lt } from "drizzle-orm";
306
+ import { cleanupClock } from "@/clocks/metrics";
284
307
  import { db, metricLogs } from "@/schema";
285
308
 
286
- export const cleanupClock = clock.every("metrics.cleanup", "1h");
287
-
288
309
  export const cleanup = on(
289
310
  cleanupClock,
290
311
  flow("metrics.cleanup", {
@@ -470,6 +491,12 @@ forms; on overlap days crontab fires **once** (first occurrence).
470
491
  that is not in the reconciled Store — check spelling against your `clock()` declarations.
471
492
  </Accordion>
472
493
 
494
+ <Accordion title="OKE1072 — Clock flow unnamed">
495
+ Cause: `A clock flow on "{trigger}" has no name.`
496
+ Fix: pass an explicit name — `on(clockDecl, flow("reports.runDaily", { do }))`.
497
+ Inline `clock.every` still needs `flow("…")` — [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
498
+ </Accordion>
499
+
473
500
  <Accordion title="DST gap / overlap warning on the row">
474
501
  Informational. Pick a UTC cron, a non-ambiguous local hour, or accept the warning. The job still
475
502
  schedules.
@@ -485,10 +512,11 @@ forms; on overlap days crontab fires **once** (first occurrence).
485
512
  ## Learn more
486
513
 
487
514
  - [Clock overview](/docs/elements/clock) — `fx.clock` helpers and drivers
515
+ - [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export) — one Flow vs shared schedule
488
516
  - [Durable Sleep](/docs/elements/clock/sleep) — `fx.clock.sleep(label, duration)`
489
517
  - [Consumers · Clock Jobs](/docs/elements/flow/consumers#clock-jobs) — bind with `on(clockDecl, flow)`
490
518
  - [fx](/docs/reference/fx) — `fx.clock.now` / `ago` / `fromNow` / `duration`
491
- - [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError`
519
+ - [Errors](/docs/reference/errors) — `ScheduleNotOverridableError` · `ClockResourceNotFoundError` · OKE1072
492
520
 
493
521
  ## Next
494
522
 
@@ -96,15 +96,21 @@ export const processEmail = on(
96
96
 
97
97
  <Tab value="Clock">
98
98
 
99
- Name the schedule, then bind it. Omit `timezone` for `"UTC"`:
99
+ Name the schedule, then bind it. One Flow can write `clock.every` inside `on()` —
100
+ [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
101
+
102
+ ```typescript title="src/clocks/metrics.ts"
103
+ import { clock } from "okengine";
104
+
105
+ export const cleanupClock = clock.every("metrics.cleanup", "1h");
106
+ ```
100
107
 
101
108
  ```typescript title="src/flows/metrics/cleanup.ts"
102
- import { on, flow, clock } from "okengine";
109
+ import { on, flow } from "okengine";
103
110
  import { lt } from "drizzle-orm";
111
+ import { cleanupClock } from "@/clocks/metrics";
104
112
  import { db, metricLogs } from "@/schema";
105
113
 
106
- export const cleanupClock = clock.every("metrics.cleanup", "1h");
107
-
108
114
  export const cleanupMetrics = on(
109
115
  cleanupClock,
110
116
  flow("metrics.cleanup", {
@@ -180,19 +186,19 @@ Omit `key` for competing consumers with no ordering.
180
186
 
181
187
  ## Trigger Reference
182
188
 
183
- | Trigger | Signature | Purpose | `do` input |
184
- | ---------- | --------------------------------------------------- | ------------------------------------------- | -------------------------------------- |
185
- | Signal | `on(signalHandle, flow)` or inline `signal.once(…)` | Queue (`once`) or fan-out (`broadcast`) | Payload (`schema`) |
186
- | Clock | `on(clockDecl, flow)` or inline `clock.every(…)` | Interval or cron tick | none (`_`) |
187
- | CDC any | `on(db.table(t).changed(), flow)` | Every insert / update / delete | `{ before, after, table, action, id }` |
188
- | CDC column | `on(db.table(t).changed("col"), flow)` | Same writes; column stamped on the Manifest | `{ before, after, table, action, id }` |
189
+ | Trigger | Signature | Purpose | `do` input |
190
+ | ---------- | ---------------------------------------------------------------- | ------------------------------------------- | -------------------------------------- |
191
+ | Signal | `on(handle, flow("name", { do }))` | Queue (`once`) or fan-out (`broadcast`) | Payload (`schema`) |
192
+ | Clock | `on(clockDecl, flow("name", { do }))` or inline `clock.every(…)` | Interval or cron tick | none (`_`) |
193
+ | CDC any | `on(db.table(t).changed(), flow)` | Every insert / update / delete | `{ before, after, table, action, id }` |
194
+ | CDC column | `on(db.table(t).changed("col"), flow)` | Same writes; column stamped on the Manifest | `{ before, after, table, action, id }` |
189
195
 
190
196
  `signal.live` is an HTTP SSE tape — bind it with [`http.live`](/docs/elements/flow/http#live-streams),
191
197
  not as a worker. A Flow with no trigger is [call-only](/docs/elements/flow).
192
198
 
193
- One-file inline vs exported handle: [Signal](/docs/elements/signal#inline-or-named-export) ·
194
- [Clock](/docs/elements/clock#inline-or-named-export). Flow name (inherit / explicit / tree):
195
- [Signal](/docs/elements/signal#flow-name) · [Clock](/docs/elements/clock#flow-name). Collision is **OKE1070**.
199
+ Signal is always an exported const (`fx.emit` needs the handle). Clock may write
200
+ `clock.every(…)` inside `on()` — [Clock · Inline or named
201
+ export](/docs/elements/clock#inline-or-named-export). **OKE1072** if nameless.
196
202
 
197
203
  ## Signal Consumers
198
204
 
@@ -336,10 +342,9 @@ Broadcast does not use the `once` lease / DLQ path. Live uses the retained tape,
336
342
  </Accordion>
337
343
 
338
344
  <Accordion title="Schema at emit">
339
- Invalid payloads fail at `fx.emit` with **OKE1250** (`"{resource}": {detail}`) before any
340
- consumer runs. Fix the payload or the Signal's `schema`. This is an **emit** contract — not
341
- an HTTP invoke contract on `flow()`. Workers inherit the typed payload from the Signal
342
- declaration; they do not declare `in` on `flow({ do })`.
345
+ Invalid payloads fail at `fx.emit` with **OKE1250** (`"{resource}": {detail}`) before any consumer
346
+ runs. This is an **emit** contract — workers inherit the payload from the Signal; they do not
347
+ declare `in` on `flow()`.
343
348
  </Accordion>
344
349
 
345
350
  </Accordions>
@@ -347,11 +352,9 @@ Broadcast does not use the `once` lease / DLQ path. Live uses the retained tape,
347
352
  ## Clock Jobs
348
353
 
349
354
  <Callout title="Detailed section">
350
- Prefer `clock.every` / `clock.daily` / `clock.cron` (and the other named helpers) for fixed
351
- schedules — they compile to the same decl as the bare callable. The lower-level bare `clock(name,
352
- options)` form still works when you need a raw expression or a schedule chosen programmatically;
353
- missing both `cron` and `every` throws `clock("name"): require cron or every`. Bind the returned
354
- handle with `on(clockDecl, flow)`.
355
+ Prefer named helpers (`clock.every` / `daily` / `cron`). Bind with
356
+ `on(clockDecl, flow("name", { do }))`, or write `clock.every` inside `on()` —
357
+ [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
355
358
  </Callout>
356
359
 
357
360
  Named clocks reconcile into the Store at boot. The scheduler leader-elects so N instances do not
@@ -363,10 +366,15 @@ double-fire. `do` receives no payload — read time through `fx.clock`.
363
366
 
364
367
  Human durations: `"200ms"` · `"30s"` · `"5m"` · `"1h"` · `"7d"` (integer + unit, no weeks):
365
368
 
366
- ```typescript title="src/flows/health/ping.ts"
367
- import { on, flow, clock } from "okengine";
369
+ ```typescript title="src/clocks/health.ts"
370
+ import { clock } from "okengine";
368
371
 
369
372
  export const pingClock = clock.every("health.pingExternal", "30s");
373
+ ```
374
+
375
+ ```typescript title="src/flows/health/ping.ts"
376
+ import { on, flow } from "okengine";
377
+ import { pingClock } from "@/clocks/health";
370
378
 
371
379
  export const pingExternal = on(
372
380
  pingClock,
@@ -385,13 +393,18 @@ export const pingExternal = on(
385
393
 
386
394
  Five-field cron (`m h dom mon dow`) plus an IANA `timezone` (default `"UTC"`):
387
395
 
388
- ```typescript title="src/flows/reports/daily.ts"
389
- import { on, flow, clock } from "okengine";
396
+ ```typescript title="src/clocks/reports.ts"
397
+ import { clock } from "okengine";
390
398
 
391
399
  export const dailyReportClock = clock.daily("reports.daily", {
392
400
  at: "06:00",
393
401
  timezone: "Asia/Riyadh",
394
402
  });
403
+ ```
404
+
405
+ ```typescript title="src/flows/reports/daily.ts"
406
+ import { on, flow } from "okengine";
407
+ import { dailyReportClock } from "@/clocks/reports";
395
408
 
396
409
  export const runDailyReport = on(
397
410
  dailyReportClock,
@@ -415,9 +428,14 @@ trigger when both are present.
415
428
  is never ticked:
416
429
 
417
430
  ```typescript title="src/clocks/invoices.ts"
418
- import { on, flow, clock } from "okengine";
431
+ import { clock } from "okengine";
419
432
 
420
433
  export const invoicesClock = clock.perTenant("invoices", { every: "1h" });
434
+ ```
435
+
436
+ ```typescript title="src/flows/billing/invoices.ts"
437
+ import { on, flow } from "okengine";
438
+ import { invoicesClock } from "@/clocks/invoices";
421
439
 
422
440
  export const runInvoices = on(
423
441
  invoicesClock,
@@ -674,9 +692,13 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
674
692
  </Accordion>
675
693
 
676
694
  <Accordion title="OKE1070 — flow name defined twice">
677
- Cause: `Flow "{flow}" is defined twice.` Two nameless `flow({ do })` bindings inherited the same
678
- Signal or Clock name, or two explicit `flow("…")` calls share a name. Give at least one an
679
- explicit `flow("…")` or a distinct tree export.
695
+ Cause: `Flow "{flow}" is defined twice.` Two `flow("…")` strings share a name. Give at least one a
696
+ distinct name.
697
+ </Accordion>
698
+
699
+ <Accordion title="OKE1072 — Signal or Clock flow unnamed">
700
+ Cause: `A {kind} flow on "{trigger}" has no name.`
701
+ Fix: pass an explicit name — `on(handle, flow("unit.export", { do }))`.
680
702
  </Accordion>
681
703
 
682
704
  <Accordion title="OKE1071 — once signal bound to more than one Flow">
@@ -733,10 +755,11 @@ Clock drivers: **postgres** in `dev`/`prod`, **frozen** in `test`. Signal driver
733
755
  - [Signal](/docs/elements/signal) — `once` / `broadcast` / `live` physics
734
756
  - [Signal · Once](/docs/elements/signal/once) — leases, retries, partition keys
735
757
  - [Clock](/docs/elements/clock) — schedules, `fx.clock.sleep`
758
+ - [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export) — one Flow vs shared schedule
736
759
  - [Store · SQL](/docs/elements/store/sql) — tables CDC watches
737
760
  - [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — `signal.live` SSE
738
761
  - [fx](/docs/reference/fx) — `fx.emit`, `fx.deadLetters`, `fx.clock`
739
- - [Errors](/docs/reference/errors) — OKE1070 · OKE1071 · OKE1240 · OKE1250
762
+ - [Errors](/docs/reference/errors) — OKE1070 · OKE1071 · OKE1072 · OKE1240 · OKE1250
740
763
  - [Workflows](/docs/elements/flow/workflows) — `durable: true` + `fx.step` on a consumer
741
764
 
742
765
  ## Next
@@ -344,9 +344,9 @@ A bare `404` with body `Not Found` means **no route matched** — not `fx.fail("
344
344
  </Accordion>
345
345
 
346
346
  <Accordion title="Name stamping">
347
- Prefer nameless `flow({ do })` on tree files — the file stamps `unit.export`.
348
- Signal / Clock inherit the trigger name only outside a unit folder
349
- ([Signal · Flow name](/docs/elements/signal#flow-name)). Nameless HTTP after adopt fails **OKE1045**.
347
+ Prefer nameless `flow({ do })` on HTTP tree files — `src/flows/notes/[id]/get.ts` +
348
+ `export const get` stamps `notes.get`. Signal / Clock workers pass `flow("name", { do })`
349
+ (**OKE1072**; Clock inline is [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export)).
350
350
  </Accordion>
351
351
 
352
352
  </Accordions>
@@ -455,18 +455,9 @@ prefix). Wrong-unit prefixes fail generate.
455
455
  Non-HTTP files still join the unit. A signal consumer in `notes/on-created.ts`
456
456
  is `api.notes.onCreated` over RPC (`POST /_oke/notes/onCreated`), not HTTP.
457
457
 
458
- Nameless Signal / Clock consumers take the trigger name only when the tree does
459
- not stamp `unit.export`:
460
-
461
- | Style | Flow name |
462
- | ------------------------------- | ----------------------- |
463
- | `flow({ do })` (no unit folder) | the Signal / Clock name |
464
- | `flow("orders.fulfill")` | the string you passed |
465
- | `src/flows/notes/on-created.ts` | `notes.onCreated` |
466
-
467
- Worked examples: [Signal · Flow name](/docs/elements/signal#flow-name) ·
468
- [Clock · Flow name](/docs/elements/clock#flow-name). Collision fails **OKE1070**.
469
- HTTP has no trigger name of this kind — nameless HTTP stays for the tree or **OKE1045**.
458
+ Signal / Clock consumers pass an explicit `flow("…")` name (**OKE1072** if nameless
459
+ outside `src/flows/<unit>/`; **OKE1070** on collision). Clock may write `clock.every(…)`
460
+ inside `on()` — [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).
470
461
 
471
462
  ## Runtime Matching
472
463
 
@@ -524,8 +515,13 @@ plus a handwritten `http.get("/notes")`.
524
515
  </Accordion>
525
516
 
526
517
  <Accordion title="OKE1070 — flow name defined twice">
527
- Cause: `Flow "{flow}" is defined twice.` Two nameless Signal/Clock consumers inherited the same
528
- name, or two explicit `flow("…")` calls collide. Give at least one a distinct name or tree export.
518
+ Cause: `Flow "{flow}" is defined twice.` Two explicit `flow("…")` calls collide, or two tree
519
+ exports stamp the same `unit.export`. Give at least one a distinct name or tree export.
520
+ </Accordion>
521
+
522
+ <Accordion title="OKE1072 — Signal or Clock flow unnamed">
523
+ Cause: `A {kind} flow on "{trigger}" has no name.`
524
+ Fix: pass an explicit name — `on(handle, flow("unit.export", { do }))`.
529
525
  </Accordion>
530
526
 
531
527
  <Accordion title="422 — path param missing from in">
@@ -550,7 +546,7 @@ plus a handwritten `http.get("/notes")`.
550
546
 
551
547
  - [HTTP](/docs/elements/flow/http) — verbs, envelopes, `http.resource`, live SSE
552
548
  - [Client](/docs/client/calling) — `api.notes.get`, REST vs RPC, `$routes`
553
- - [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070
549
+ - [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070 · OKE1072
554
550
  - [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
555
551
  - [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on the same trigger
556
552
 
@@ -466,6 +466,11 @@ dead-letter queue for exhausted retries in the queue sense — see
466
466
  Cause: `"{resource}": {detail}`. Align the payload with `schema` — no subscriber started.
467
467
  </Accordion>
468
468
 
469
+ <Accordion title="OKE1072 — Signal flow unnamed">
470
+ Cause: `A signal flow on "{trigger}" has no name.`
471
+ Fix: pass an explicit name — `on(handle, flow("cache.purgeLocal", { do }))`.
472
+ </Accordion>
473
+
469
474
  <Accordion title="Expecting retries / DLQ like a queue">
470
475
  Use `signal.once`. Broadcast fan-out is not the lease + DLQ path documented under
471
476
  [Once](/docs/elements/signal/once).
@@ -492,7 +497,7 @@ dead-letter queue for exhausted retries in the queue sense — see
492
497
  - [Consumers](/docs/elements/flow/consumers) — binding `on(signal)`
493
498
  - [HTTP · Live Streams](/docs/elements/flow/http#live-streams) — when the consumer is a browser
494
499
  - [fx](/docs/reference/fx) — `fx.emit`
495
- - [Errors](/docs/reference/errors) — OKE1240 · OKE1250
500
+ - [Errors](/docs/reference/errors) — OKE1072 · OKE1240 · OKE1250
496
501
 
497
502
  ## Next
498
503