@blokjs/runner 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/Configuration.d.ts +18 -0
  2. package/dist/Configuration.js +151 -4
  3. package/dist/Configuration.js.map +1 -1
  4. package/dist/PayloadTooLargeError.d.ts +19 -0
  5. package/dist/PayloadTooLargeError.js +29 -0
  6. package/dist/PayloadTooLargeError.js.map +1 -0
  7. package/dist/RunCancelledError.d.ts +17 -0
  8. package/dist/RunCancelledError.js +25 -0
  9. package/dist/RunCancelledError.js.map +1 -0
  10. package/dist/RunnerSteps.js +330 -33
  11. package/dist/RunnerSteps.js.map +1 -1
  12. package/dist/SubworkflowNode.d.ts +75 -0
  13. package/dist/SubworkflowNode.js +221 -0
  14. package/dist/SubworkflowNode.js.map +1 -0
  15. package/dist/TriggerBase.d.ts +128 -0
  16. package/dist/TriggerBase.js +773 -4
  17. package/dist/TriggerBase.js.map +1 -1
  18. package/dist/WaitDispatchRequest.d.ts +38 -0
  19. package/dist/WaitDispatchRequest.js +13 -0
  20. package/dist/WaitDispatchRequest.js.map +1 -0
  21. package/dist/WaitNode.d.ts +23 -0
  22. package/dist/WaitNode.js +26 -0
  23. package/dist/WaitNode.js.map +1 -0
  24. package/dist/concurrency/ConcurrencyBackend.d.ts +61 -0
  25. package/dist/concurrency/ConcurrencyBackend.js +20 -0
  26. package/dist/concurrency/ConcurrencyBackend.js.map +1 -0
  27. package/dist/concurrency/ConcurrencyLimitError.d.ts +37 -0
  28. package/dist/concurrency/ConcurrencyLimitError.js +16 -0
  29. package/dist/concurrency/ConcurrencyLimitError.js.map +1 -0
  30. package/dist/concurrency/NatsKvConcurrencyBackend.d.ts +64 -0
  31. package/dist/concurrency/NatsKvConcurrencyBackend.js +297 -0
  32. package/dist/concurrency/NatsKvConcurrencyBackend.js.map +1 -0
  33. package/dist/concurrency/QueueExpiredError.d.ts +40 -0
  34. package/dist/concurrency/QueueExpiredError.js +15 -0
  35. package/dist/concurrency/QueueExpiredError.js.map +1 -0
  36. package/dist/concurrency/createConcurrencyBackend.d.ts +23 -0
  37. package/dist/concurrency/createConcurrencyBackend.js +34 -0
  38. package/dist/concurrency/createConcurrencyBackend.js.map +1 -0
  39. package/dist/concurrency/readConcurrencyConfig.d.ts +60 -0
  40. package/dist/concurrency/readConcurrencyConfig.js +60 -0
  41. package/dist/concurrency/readConcurrencyConfig.js.map +1 -0
  42. package/dist/idempotency/resolveIdempotencyKey.d.ts +20 -0
  43. package/dist/idempotency/resolveIdempotencyKey.js +37 -0
  44. package/dist/idempotency/resolveIdempotencyKey.js.map +1 -0
  45. package/dist/index.d.ts +23 -3
  46. package/dist/index.js +47 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/monitoring/ConcurrencyMetrics.d.ts +56 -0
  49. package/dist/monitoring/ConcurrencyMetrics.js +107 -0
  50. package/dist/monitoring/ConcurrencyMetrics.js.map +1 -0
  51. package/dist/monitoring/JanitorMetrics.d.ts +27 -0
  52. package/dist/monitoring/JanitorMetrics.js +48 -0
  53. package/dist/monitoring/JanitorMetrics.js.map +1 -0
  54. package/dist/scheduling/DebounceCoordinator.d.ts +88 -0
  55. package/dist/scheduling/DebounceCoordinator.js +141 -0
  56. package/dist/scheduling/DebounceCoordinator.js.map +1 -0
  57. package/dist/scheduling/DeferredDispatchSignal.d.ts +50 -0
  58. package/dist/scheduling/DeferredDispatchSignal.js +14 -0
  59. package/dist/scheduling/DeferredDispatchSignal.js.map +1 -0
  60. package/dist/scheduling/DeferredRunScheduler.d.ts +68 -0
  61. package/dist/scheduling/DeferredRunScheduler.js +154 -0
  62. package/dist/scheduling/DeferredRunScheduler.js.map +1 -0
  63. package/dist/scheduling/readSchedulingConfig.d.ts +24 -0
  64. package/dist/scheduling/readSchedulingConfig.js +52 -0
  65. package/dist/scheduling/readSchedulingConfig.js.map +1 -0
  66. package/dist/timeouts/StepTimeoutError.d.ts +22 -0
  67. package/dist/timeouts/StepTimeoutError.js +31 -0
  68. package/dist/timeouts/StepTimeoutError.js.map +1 -0
  69. package/dist/tracing/InMemoryRunStore.d.ts +28 -1
  70. package/dist/tracing/InMemoryRunStore.js +150 -0
  71. package/dist/tracing/InMemoryRunStore.js.map +1 -1
  72. package/dist/tracing/Janitor.d.ts +70 -0
  73. package/dist/tracing/Janitor.js +150 -0
  74. package/dist/tracing/Janitor.js.map +1 -0
  75. package/dist/tracing/PostgresRunStore.d.ts +30 -0
  76. package/dist/tracing/PostgresRunStore.js +435 -3
  77. package/dist/tracing/PostgresRunStore.js.map +1 -1
  78. package/dist/tracing/RunStore.d.ts +100 -1
  79. package/dist/tracing/RunTracker.d.ts +238 -9
  80. package/dist/tracing/RunTracker.js +571 -1
  81. package/dist/tracing/RunTracker.js.map +1 -1
  82. package/dist/tracing/SqliteRunStore.d.ts +23 -1
  83. package/dist/tracing/SqliteRunStore.js +405 -6
  84. package/dist/tracing/SqliteRunStore.js.map +1 -1
  85. package/dist/tracing/TraceRouter.d.ts +20 -2
  86. package/dist/tracing/TraceRouter.js +249 -5
  87. package/dist/tracing/TraceRouter.js.map +1 -1
  88. package/dist/tracing/sanitize.d.ts +11 -0
  89. package/dist/tracing/sanitize.js +29 -0
  90. package/dist/tracing/sanitize.js.map +1 -1
  91. package/dist/tracing/types.d.ts +348 -2
  92. package/dist/utils/createChildContext.d.ts +32 -0
  93. package/dist/utils/createChildContext.js +113 -0
  94. package/dist/utils/createChildContext.js.map +1 -0
  95. package/dist/workflow/WorkflowNormalizer.d.ts +29 -41
  96. package/dist/workflow/WorkflowNormalizer.js +182 -0
  97. package/dist/workflow/WorkflowNormalizer.js.map +1 -1
  98. package/dist/workflow/WorkflowRegistry.d.ts +64 -0
  99. package/dist/workflow/WorkflowRegistry.js +81 -0
  100. package/dist/workflow/WorkflowRegistry.js.map +1 -0
  101. package/package.json +3 -3
@@ -1,4 +1,37 @@
1
- export type WorkflowRunStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
1
+ /**
2
+ * Lifecycle status of a workflow run.
3
+ *
4
+ * - `pending`: created but not yet started.
5
+ * - `running`: currently executing.
6
+ * - `completed`: finished successfully.
7
+ * - `failed`: a step threw or the run aborted with an error.
8
+ * - `cancelled`: stopped externally before completion.
9
+ * - `throttled` (Tier 2 #6): rejected at run-entry because the
10
+ * concurrency limit for the resolved `concurrencyKey` was reached.
11
+ * Distinct from `failed` — no step ran; nothing produced an error.
12
+ * - `delayed` (Tier 2 #5): scheduled to start at a future time. The
13
+ * run record exists; the dispatch is pending. Transitions to
14
+ * `running` when the timer fires.
15
+ * - `expired` (Tier 2 #5): TTL exceeded before dispatch. Auto-cancelled
16
+ * without execution. Distinct from `cancelled` (which is operator-
17
+ * initiated) and `failed` (which implies a step ran).
18
+ * - `debounced` (Tier 2 #7): coalesced into another run via the
19
+ * `debounce.key` mechanism. The "loser" of a leading-mode coalesce;
20
+ * in trailing mode this state is transient (run flips to `running`
21
+ * when the debounce timer fires).
22
+ * - `crashed` (Tier 2 quick-wins): the runner itself crashed (uncaught
23
+ * exception, OOM, signal). Distinct from `failed` (which implies a
24
+ * step's `process()` threw cleanly). Currently MANUAL — call
25
+ * `tracker.markRunCrashed(runId, {error})` from custom triggers /
26
+ * ops harnesses. Auto-flip on uncaught TriggerBase errors is a
27
+ * deferred follow-up.
28
+ * - `timedOut` (Tier 2 quick-wins): a step's final retry attempt
29
+ * exceeded its `maxDuration` cap. Distinct from `failed` so SLA
30
+ * dashboards can separate timeout-driven failures (network /
31
+ * capacity) from logic failures (bugs). Auto-flipped by
32
+ * `RunnerSteps` on final-attempt `StepTimeoutError`.
33
+ */
34
+ export type WorkflowRunStatus = "pending" | "running" | "completed" | "failed" | "cancelled" | "throttled" | "delayed" | "expired" | "debounced" | "queued" | "crashed" | "timedOut";
2
35
  /**
3
36
  * Structured failure detail attached to a {@link WorkflowRun} or
4
37
  * {@link NodeRun}. When the failure source threw a typed `BlokError`
@@ -60,6 +93,74 @@ export interface WorkflowRun {
60
93
  * SQLite databases still work because the column is optional.
61
94
  */
62
95
  environment?: string;
96
+ /**
97
+ * Tier 1 · replay lineage. When this run was started via
98
+ * `POST /__blok/runs/:id/replay`, this carries the original run's id.
99
+ * Studio renders a "Replay of #..." breadcrumb that links back to the
100
+ * source run. Absent on first-class triggered runs.
101
+ *
102
+ * Plumbed end-to-end via the `X-Blok-Replay-Of` HTTP header that the
103
+ * replay endpoint sets on the dispatched request. TriggerBase reads
104
+ * the header and threads it into `tracker.startRun({ replayOf })`.
105
+ */
106
+ replayOf?: string;
107
+ /**
108
+ * Tier 2 · sub-workflow lineage. When this run was started by a
109
+ * `subworkflow:` step in another workflow, this carries the parent
110
+ * run's id. Studio renders a "called from #..." breadcrumb that
111
+ * links back to the parent run. Absent on first-class triggered runs.
112
+ */
113
+ parentRunId?: string;
114
+ /**
115
+ * Tier 2 · sub-workflow lineage. The specific NodeRun within the
116
+ * parent run that invoked this sub-workflow — lets Studio jump to
117
+ * the exact sub-workflow step on the parent's run-detail page.
118
+ */
119
+ parentNodeRunId?: string;
120
+ /**
121
+ * Tier 2 #5 · scheduled dispatch time (ms since epoch). Set when the
122
+ * run is created with `delay` on the trigger config, OR when the
123
+ * debounce coordinator parks a coalescing run. Absent on immediate
124
+ * runs.
125
+ */
126
+ scheduledAt?: number;
127
+ /**
128
+ * Tier 2 #5 · TTL deadline (ms since epoch). When `now > expiresAt`
129
+ * at dispatch time, the run is marked `expired` and skipped. Set
130
+ * from `trigger.ttl` plus the original submission timestamp. Absent
131
+ * when no TTL is configured.
132
+ */
133
+ expiresAt?: number;
134
+ /**
135
+ * Tier 2 #7 · resolved debounce key (`debounce.key` after
136
+ * `js/...`-expression evaluation). Pings sharing this key + workflow
137
+ * name coalesce into the same `WorkflowRun`. Absent on non-debounced
138
+ * runs.
139
+ */
140
+ debounceKey?: string;
141
+ /**
142
+ * Tier 2 #7 · debounce mode that produced this run. Surfaces in
143
+ * Studio's run detail so users can tell `leading` (immediate-fire)
144
+ * vs `trailing` (silence-then-fire) at a glance.
145
+ */
146
+ debounceMode?: "leading" | "trailing";
147
+ /**
148
+ * Tier 2 #7 · number of pings absorbed by this run before dispatch.
149
+ * Starts at 1 (the first ping). Subsequent same-key pings increment
150
+ * this rather than creating new run records. Surfaces on Studio's
151
+ * run row as "Pings: N".
152
+ */
153
+ pingCount?: number;
154
+ /**
155
+ * PR 4 · `wait.for(duration)` / `wait.until(date)` resume cursor.
156
+ *
157
+ * Set after each non-wait step completes. On
158
+ * `dispatchDeferred` re-entry from a wait step, the runner skips
159
+ * steps with `stepIndex <= lastCompletedStepIndex` to avoid
160
+ * re-running pre-wait steps. Undefined for runs that never
161
+ * encountered a wait step (preserves existing semantics).
162
+ */
163
+ lastCompletedStepIndex?: number;
63
164
  }
64
165
  export type NodeRunStatus = "pending" | "running" | "completed" | "failed" | "skipped";
65
166
  export interface NodeRun {
@@ -113,12 +214,199 @@ export interface NodeRun {
113
214
  /** Bytes received from the SDK in the response. */
114
215
  response_bytes?: number;
115
216
  };
217
+ /**
218
+ * Tier 1 idempotency cache lineage. Populated by `RunTracker.markNodeCached`
219
+ * when the step short-circuited via a cache hit instead of running. Studio
220
+ * renders a "CACHED" badge with a click-through to the source run/node.
221
+ * Absent on regular completed nodes.
222
+ */
223
+ cached?: {
224
+ sourceRunId: string;
225
+ sourceNodeRunId: string;
226
+ cachedAt: number;
227
+ };
228
+ /**
229
+ * Tier 1 retry attempts. One entry per `NODE_ATTEMPT_FAILED` event the
230
+ * step emitted before either succeeding or exhausting `retry.maxAttempts`.
231
+ * Capped at 10 entries (`MAX_STORED_ATTEMPTS`) — extreme retry counts
232
+ * are bounded so a runaway loop can't bloat the run store.
233
+ *
234
+ * Studio renders this as a collapsible "Attempts (N)" disclosure on the
235
+ * step's panel. The final outcome lives on the node's status / error
236
+ * fields; this array carries only the failed attempts that came before.
237
+ */
238
+ attempts?: Array<{
239
+ attempt: number;
240
+ error: RunErrorDetail;
241
+ timestamp: number;
242
+ }>;
243
+ /**
244
+ * Tier 2 #4 sub-workflow mode — only set for `nodeType === "subworkflow"`.
245
+ * - `true` (default) — synchronous: parent step blocks on child completion.
246
+ * - `false` — fire-and-forget: parent returns dispatch metadata immediately;
247
+ * child runs asynchronously via `setImmediate`. Studio renders a distinct
248
+ * `↳ async` badge in StepRail to differentiate from synchronous siblings.
249
+ *
250
+ * Captured at `tracker.startNode()` time so it survives the trace and is
251
+ * visible to Studio without recomputing from outputs heuristics.
252
+ */
253
+ wait?: boolean;
254
+ /**
255
+ * PR 5 E3 — sub-workflow nesting depth surfaced for Studio.
256
+ *
257
+ * Set on subworkflow node runs. Top-level workflow's sub-workflow
258
+ * step has `subworkflowDepth = 1`; that child's sub-workflow step
259
+ * has `subworkflowDepth = 2`; etc. Bounded by
260
+ * `BLOK_MAX_SUBWORKFLOW_DEPTH` (default 10).
261
+ *
262
+ * Studio renders `↳ async (N)` / `↳ sub (N)` only when N >= 2 to
263
+ * keep top-level invocations un-cluttered.
264
+ */
265
+ subworkflowDepth?: number;
266
+ }
267
+ /**
268
+ * Stored result of a previously-successful step execution, keyed by
269
+ * `(workflowName, step.id, idempotencyKey)`. On cache hit, RunnerSteps
270
+ * skips `step.process()` and replays the cached `data` through the same
271
+ * `PersistenceHelper.applyStepOutput` rules — caching layers ABOVE
272
+ * persistence, never within it.
273
+ */
274
+ export interface CachedStepResult {
275
+ /** The data the step originally returned. */
276
+ data: unknown;
277
+ /** ms since epoch when the entry was written. */
278
+ cachedAt: number;
279
+ /** ms since epoch when the entry expires; null = no expiry. */
280
+ expiresAt: number | null;
281
+ /** Run that originally produced this result. */
282
+ sourceRunId: string;
283
+ /** Node run that originally produced this result. */
284
+ sourceNodeRunId: string;
285
+ }
286
+ /**
287
+ * Outcome of an `acquireConcurrencySlot` attempt (Tier 2 #6).
288
+ *
289
+ * The store's gate decides whether the run is allowed to proceed by
290
+ * comparing the current in-flight count for the (workflow, key) pair
291
+ * against the requested limit.
292
+ */
293
+ export interface ConcurrencySlotResult {
294
+ /** True when the slot was granted; false when the run should be throttled. */
295
+ acquired: boolean;
296
+ /**
297
+ * Number of in-flight runs (including the just-acquired one when
298
+ * `acquired === true`) sharing the same (workflowName, concurrencyKey).
299
+ * Useful for observability — Studio surfaces this on `RUN_THROTTLED`.
300
+ */
301
+ currentInFlight: number;
302
+ }
303
+ /**
304
+ * A persisted scheduled dispatch — one row per pending HTTP-trigger
305
+ * deferral. Written by `DeferredRunScheduler.schedule()` when a
306
+ * `persist` payload is provided; deleted on cancel or fire.
307
+ *
308
+ * Boot recovery (HttpTrigger.recoverDispatches) scans this table,
309
+ * marks past-due+TTL-expired rows as `"expired"`, and re-registers
310
+ * timers for live dispatches.
311
+ */
312
+ export interface ScheduledDispatchRow {
313
+ runId: string;
314
+ workflowName: string;
315
+ /** `"http"` for v1; future triggers can opt in. */
316
+ triggerType: string;
317
+ /** ms since epoch when to dispatch. */
318
+ scheduledAt: number;
319
+ /** ms since epoch TTL deadline (undefined = no TTL). */
320
+ expiresAt?: number;
321
+ /** Mirrors the run record's status — `"delayed" | "queued" | "debounced"`. */
322
+ dispatchStatus: "delayed" | "queued" | "debounced";
323
+ /**
324
+ * JSON-serialized minimal Context subset, trigger-defined.
325
+ * For HTTP: `{method, path, headers, body, params, query, workflowPath}`
326
+ * with sensitive header keys stripped (authorization, cookie, x-api-key).
327
+ */
328
+ payload: unknown;
329
+ /** ms since epoch when the row was first written. */
330
+ createdAt: number;
116
331
  }
117
332
  export type RunEventType = "RUN_STARTED" | "RUN_COMPLETED" | "RUN_FAILED" | "NODE_STARTED" | "NODE_COMPLETED" | "NODE_FAILED" | "NODE_SKIPPED" | "VARS_UPDATED" | "LOG_ENTRY"
118
333
  /** §17 Phase 5: streaming `Progress` frame from the SDK. */
119
334
  | "NODE_PROGRESS"
120
335
  /** §17 Phase 5: streaming `PartialResult` frame from the SDK. */
121
- | "NODE_PARTIAL_RESULT";
336
+ | "NODE_PARTIAL_RESULT"
337
+ /**
338
+ * Tier 1 idempotency cache hit: the step short-circuited via the
339
+ * idempotency cache instead of running. Payload carries
340
+ * `{ durationMs, source: { sourceRunId, sourceNodeRunId, cachedAt } }`.
341
+ * Studio renders a CACHED badge on the affected node.
342
+ */
343
+ | "NODE_CACHED"
344
+ /**
345
+ * Tier 1 retry: a step attempt failed and another retry will follow.
346
+ * Payload carries `{ attempt, error }`. Final attempt failure (after
347
+ * exhausting `retry.maxAttempts`) emits `NODE_FAILED` instead.
348
+ */
349
+ | "NODE_ATTEMPT_FAILED"
350
+ /**
351
+ * Tier 2 #6 concurrency gate denied a run before any step executed.
352
+ * Payload carries `{ concurrencyKey, concurrencyLimit, currentInFlight }`.
353
+ * The run's status flips to `"throttled"` (distinct from `"failed"` —
354
+ * no step ran). Studio surfaces a Throttled badge.
355
+ */
356
+ | "RUN_THROTTLED"
357
+ /**
358
+ * Tier 2 #5 — run scheduled for later dispatch via `trigger.delay`.
359
+ * Payload `{scheduledAt, delayMs, expiresAt?}`. Status flips to
360
+ * `"delayed"`; transitions to `"running"` when the timer fires.
361
+ */
362
+ | "RUN_DELAYED"
363
+ /**
364
+ * Tier 2 #5 — TTL exceeded; run auto-cancelled before dispatch.
365
+ * Payload `{expiresAt, expiredAt, lateBy}`. Status flips to `"expired"`.
366
+ */
367
+ | "RUN_EXPIRED"
368
+ /**
369
+ * Tier 2 #7 — run coalesced into a debounce window. Payload
370
+ * `{debounceKey, mode, intoRunId?, pingCount?}`. Status flips to
371
+ * `"debounced"`. In leading mode this is terminal (the run never
372
+ * executes — execution went to a sibling). In trailing mode this is
373
+ * transient and the same run flips to `"running"` when the window
374
+ * closes.
375
+ */
376
+ | "RUN_DEBOUNCED"
377
+ /**
378
+ * Tier 2 #6 follow-up — concurrency gate denied a run AND the trigger
379
+ * is configured with `onLimit: "queue"`. Instead of throwing, the run
380
+ * is deferred via `DeferredRunScheduler` and re-attempts acquisition
381
+ * after a 1s delay. Payload `{concurrencyKey, concurrencyLimit,
382
+ * currentInFlight, scheduledAt}`. Status flips to `"queued"`;
383
+ * transitions to `"running"` when the timer fires (and may flip back
384
+ * to `"queued"` on re-denial).
385
+ */
386
+ | "RUN_QUEUED"
387
+ /**
388
+ * Tier 2 polish — operator cancelled a pending (delayed/debounced/
389
+ * queued) run via `POST /__blok/runs/:runId/cancel`. Payload
390
+ * `{durationMs, previousStatus}`. Status flips to `"cancelled"`.
391
+ * Currently only pre-execution states are cancellable; running runs
392
+ * require cooperative `AbortSignal` (deferred follow-up).
393
+ */
394
+ | "RUN_CANCELLED"
395
+ /**
396
+ * Tier 2 quick-wins — run crashed (uncaught exception / OOM /
397
+ * signal). Payload `{durationMs, error}`. Distinct from `RUN_FAILED`
398
+ * (which implies a step's `process()` threw cleanly). Currently
399
+ * manual via `tracker.markRunCrashed(runId, {error})`.
400
+ */
401
+ | "RUN_CRASHED"
402
+ /**
403
+ * Tier 2 quick-wins — a step's final retry attempt exceeded its
404
+ * `maxDuration` cap. Payload `{durationMs, stepId, maxDurationMs,
405
+ * attemptsExhausted}`. Auto-emitted by `RunnerSteps` when the
406
+ * timeout fires on the last attempt. Run status flips to
407
+ * `"timedOut"`.
408
+ */
409
+ | "RUN_TIMED_OUT";
122
410
  export interface RunEvent {
123
411
  id: string;
124
412
  type: RunEventType;
@@ -170,6 +458,43 @@ export interface StartRunOptions {
170
458
  nodeCount: number;
171
459
  tags?: string[];
172
460
  metadata?: Record<string, unknown>;
461
+ /**
462
+ * Tier 1 · replay lineage. When a run is started via the replay endpoint,
463
+ * this carries the original run's id so Studio can render a "Replay of #..."
464
+ * breadcrumb. Plumbed end-to-end via the `X-Blok-Replay-Of` HTTP header.
465
+ */
466
+ replayOf?: string;
467
+ /**
468
+ * Tier 2 · sub-workflow lineage. Populated by `SubworkflowNode` when
469
+ * invoking a child workflow inline. Studio uses this to render a
470
+ * "called from #..." breadcrumb on the child's run detail.
471
+ */
472
+ parentRunId?: string;
473
+ parentNodeRunId?: string;
474
+ /**
475
+ * Tier 2 #5 · scheduled dispatch time (ms since epoch). When set, the
476
+ * run is created with status `"delayed"` instead of `"running"`.
477
+ */
478
+ scheduledAt?: number;
479
+ /**
480
+ * Tier 2 #5 · TTL deadline (ms since epoch). Persists onto the run
481
+ * for the dispatcher to consult.
482
+ */
483
+ expiresAt?: number;
484
+ /**
485
+ * Tier 2 #7 · resolved debounce key. When set, pings sharing this
486
+ * key + workflow name coalesce.
487
+ */
488
+ debounceKey?: string;
489
+ /**
490
+ * Tier 2 #7 · debounce mode that produced this run.
491
+ */
492
+ debounceMode?: "leading" | "trailing";
493
+ /**
494
+ * Tier 2 #7 · initial ping count for the run (typically 1 — the
495
+ * first ping). Subsequent pings increment via direct store update.
496
+ */
497
+ pingCount?: number;
173
498
  }
174
499
  export interface StartNodeOptions {
175
500
  nodeName: string;
@@ -179,6 +504,18 @@ export interface StartNodeOptions {
179
504
  parentNodeId?: string;
180
505
  depth: number;
181
506
  stepIndex: number;
507
+ /**
508
+ * Tier 2 #4 sub-workflow mode. Only meaningful when `nodeType === "subworkflow"`.
509
+ * Persisted onto `NodeRun.wait` so Studio can render `↳ async` vs `↳ sub`.
510
+ */
511
+ wait?: boolean;
512
+ /**
513
+ * PR 5 E3 — sub-workflow nesting depth. Only meaningful when
514
+ * `nodeType === "subworkflow"`. The runner computes
515
+ * `parentDepth + 1` from `ctx._subworkflowDepth` at start-time and
516
+ * passes here so Studio can render `↳ sub (N)` for N >= 2.
517
+ */
518
+ subworkflowDepth?: number;
182
519
  }
183
520
  export type WidgetType = "stat-card" | "timeline" | "error-rate" | "duration-distribution" | "workflow-breakdown" | "node-performance" | "recent-runs" | "heatmap";
184
521
  export interface DashboardWidget {
@@ -212,6 +549,15 @@ export interface RunQuery {
212
549
  workflow?: string;
213
550
  status?: WorkflowRunStatus;
214
551
  tags?: string[];
552
+ /**
553
+ * Tier 2 quick-wins — filter by metadata key=value pairs. Multiple
554
+ * pairs combine with AND semantics (a run matches only when all
555
+ * declared keys match the requested values, compared via stringified
556
+ * equality). Backed by `json_extract` on SQLite + `Object` lookup
557
+ * on InMemory. Sequential scan (no index); acceptable given the
558
+ * `evictOldRuns` size cap on the runs table.
559
+ */
560
+ metadata?: Record<string, string>;
215
561
  limit?: number;
216
562
  offset?: number;
217
563
  sort?: "asc" | "desc";
@@ -0,0 +1,32 @@
1
+ import type { Context } from "@blokjs/shared";
2
+ /**
3
+ * Construct a fresh `Context` for a sub-workflow invocation.
4
+ *
5
+ * **Isolation contract**: the child gets fresh `state`, fresh `response`,
6
+ * fresh `error`, and a fresh `id`. The child cannot read or mutate the
7
+ * parent's state — sub-workflows are referentially transparent at the
8
+ * state-passing boundary. Parent passes data in via `request.body`
9
+ * (mirrors HTTP semantics), child returns data via `ctx.response`.
10
+ *
11
+ * **Shared by reference (intentional)**: the `logger`, `env`, and
12
+ * `eventLogger` are shared with the parent — log routing stays
13
+ * consistent and ENV is process-global anyway. The runner's tracing
14
+ * layer (`_traceRunId`, `_traceNodeId`) is set separately by
15
+ * `SubworkflowNode` after this function returns.
16
+ *
17
+ * Mirrors `TriggerBase.createContext` shape one-for-one (same `req`/
18
+ * `prev` getters, same `state`/`vars` aliasing, same `publish`
19
+ * default). Kept as a standalone helper rather than a TriggerBase
20
+ * method so sub-workflow dispatch doesn't depend on having a
21
+ * TriggerBase instance.
22
+ */
23
+ export declare function createChildContext(parent: Context, opts: {
24
+ /** The child workflow's `name:` field. */
25
+ workflowName: string;
26
+ /** Filesystem path or `"<inline>"` — used for trace + diagnostics. */
27
+ workflowPath: string;
28
+ /** Parent step's resolved inputs, becomes child's `request.body`. */
29
+ body: unknown;
30
+ /** Child's resolved `nodes` map (from child Configuration). Powers blueprint mapper. */
31
+ config: Context["config"];
32
+ }): Context;
@@ -0,0 +1,113 @@
1
+ import { v4 as uuid } from "uuid";
2
+ /**
3
+ * Construct a fresh `Context` for a sub-workflow invocation.
4
+ *
5
+ * **Isolation contract**: the child gets fresh `state`, fresh `response`,
6
+ * fresh `error`, and a fresh `id`. The child cannot read or mutate the
7
+ * parent's state — sub-workflows are referentially transparent at the
8
+ * state-passing boundary. Parent passes data in via `request.body`
9
+ * (mirrors HTTP semantics), child returns data via `ctx.response`.
10
+ *
11
+ * **Shared by reference (intentional)**: the `logger`, `env`, and
12
+ * `eventLogger` are shared with the parent — log routing stays
13
+ * consistent and ENV is process-global anyway. The runner's tracing
14
+ * layer (`_traceRunId`, `_traceNodeId`) is set separately by
15
+ * `SubworkflowNode` after this function returns.
16
+ *
17
+ * Mirrors `TriggerBase.createContext` shape one-for-one (same `req`/
18
+ * `prev` getters, same `state`/`vars` aliasing, same `publish`
19
+ * default). Kept as a standalone helper rather than a TriggerBase
20
+ * method so sub-workflow dispatch doesn't depend on having a
21
+ * TriggerBase instance.
22
+ */
23
+ export function createChildContext(parent, opts) {
24
+ const id = uuid();
25
+ const request = {
26
+ body: opts.body ?? {},
27
+ headers: {},
28
+ params: {},
29
+ query: {},
30
+ };
31
+ const response = {
32
+ data: "",
33
+ contentType: "",
34
+ success: true,
35
+ error: null,
36
+ };
37
+ const state = {};
38
+ // Tier 2 follow-up · cooperative cancellation. Child gets its own
39
+ // AbortController so it has independent lifecycle (cancellation of
40
+ // THIS sub-workflow doesn't propagate up to the parent). But we
41
+ // chain off the parent's signal: if the parent gets cancelled, the
42
+ // child should abort too — otherwise long-running sub-workflows
43
+ // would orphan when the parent exits.
44
+ //
45
+ // PR 1 follow-up · A3 fix. `addEventListener({once: true})` only
46
+ // auto-removes the listener when abort fires. If the parent never
47
+ // aborts AND the parent ctx outlives many child invocations, listeners
48
+ // accumulate and Node's MaxListenersExceededWarning fires at the 11th.
49
+ // Pass a child-scoped AbortSignal as the listener's `signal:` option
50
+ // so the listener auto-removes when the child completes (the
51
+ // SubworkflowNode dispatch path calls `listenerCleanup.abort()` in
52
+ // its finally block).
53
+ const childAbortController = new AbortController();
54
+ const listenerCleanup = new AbortController();
55
+ if (parent.signal) {
56
+ if (parent.signal.aborted) {
57
+ childAbortController.abort();
58
+ }
59
+ else {
60
+ parent.signal.addEventListener("abort", () => {
61
+ if (!childAbortController.signal.aborted)
62
+ childAbortController.abort();
63
+ }, { once: true, signal: listenerCleanup.signal });
64
+ }
65
+ }
66
+ const ctx = {
67
+ id,
68
+ workflow_name: opts.workflowName,
69
+ workflow_path: opts.workflowPath,
70
+ config: opts.config,
71
+ request,
72
+ response,
73
+ error: { message: [] },
74
+ logger: parent.logger,
75
+ eventLogger: parent.eventLogger ?? null,
76
+ // Fresh state map — child runs in isolation. Aliased as `vars`
77
+ // for v1 back-compat, same shape as `TriggerBase.createContext`.
78
+ state,
79
+ vars: state,
80
+ env: parent.env,
81
+ signal: childAbortController.signal,
82
+ // Stash the controller in a child-specific _PRIVATE_ slot so the
83
+ // tracker can fire it via abortRunningRun on direct sub-run cancel.
84
+ // Don't mutate the parent's _PRIVATE_ (intentional isolation).
85
+ // `listenerCleanup` is exposed so SubworkflowNode can abort it on
86
+ // child completion, which removes the parent.signal listener (PR 1
87
+ // A3 fix — prevents listener accumulation on long-lived parents).
88
+ _PRIVATE_: { abortController: childAbortController, listenerCleanup },
89
+ };
90
+ // V2 read-only aliases — same object reference, no copy. Mirrors
91
+ // `TriggerBase.createContext`.
92
+ Object.defineProperty(ctx, "req", {
93
+ get() {
94
+ return ctx.request;
95
+ },
96
+ enumerable: true,
97
+ });
98
+ Object.defineProperty(ctx, "prev", {
99
+ get() {
100
+ return ctx.response;
101
+ },
102
+ enumerable: true,
103
+ });
104
+ // Default `publish` — writes to state, no side-channel event. The
105
+ // triggers' production createContext also wires Studio trace
106
+ // events; for sub-workflows we omit that (the child has its own
107
+ // trace run, events fire there).
108
+ ctx.publish = (name, value) => {
109
+ ctx.state[name] = value;
110
+ };
111
+ return ctx;
112
+ }
113
+ //# sourceMappingURL=createChildContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createChildContext.js","sourceRoot":"","sources":["../../src/utils/createChildContext.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,IAAI,IAAI,EAAE,MAAM,MAAM,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,kBAAkB,CACjC,MAAe,EACf,IASC;IAED,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC;IAClB,MAAM,OAAO,GAAuB;QACnC,IAAI,EAAG,IAAI,CAAC,IAAmC,IAAI,EAAE;QACrD,OAAO,EAAE,EAAmC;QAC5C,MAAM,EAAE,EAAkC;QAC1C,KAAK,EAAE,EAAiC;KAClB,CAAC;IACxB,MAAM,QAAQ,GAAwB;QACrC,IAAI,EAAE,EAAE;QACR,WAAW,EAAE,EAAE;QACf,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,IAAI;KACY,CAAC;IACzB,MAAM,KAAK,GAA4B,EAAE,CAAC;IAE1C,kEAAkE;IAClE,mEAAmE;IACnE,gEAAgE;IAChE,mEAAmE;IACnE,gEAAgE;IAChE,sCAAsC;IACtC,EAAE;IACF,iEAAiE;IACjE,kEAAkE;IAClE,uEAAuE;IACvE,uEAAuE;IACvE,qEAAqE;IACrE,6DAA6D;IAC7D,mEAAmE;IACnE,sBAAsB;IACtB,MAAM,oBAAoB,GAAG,IAAI,eAAe,EAAE,CAAC;IACnD,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;IAC9C,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAC3B,oBAAoB,CAAC,KAAK,EAAE,CAAC;QAC9B,CAAC;aAAM,CAAC;YACP,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAC7B,OAAO,EACP,GAAG,EAAE;gBACJ,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,OAAO;oBAAE,oBAAoB,CAAC,KAAK,EAAE,CAAC;YACxE,CAAC,EACD,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,CAAC,MAAM,EAAE,CAC9C,CAAC;QACH,CAAC;IACF,CAAC;IAED,MAAM,GAAG,GAAY;QACpB,EAAE;QACF,aAAa,EAAE,IAAI,CAAC,YAAY;QAChC,aAAa,EAAE,IAAI,CAAC,YAAY;QAChC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO;QACP,QAAQ;QACR,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAsB;QAC1C,MAAM,EAAE,MAAM,CAAC,MAAuB;QACtC,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,IAAI;QACvC,+DAA+D;QAC/D,iEAAiE;QACjE,KAAK;QACL,IAAI,EAAE,KAAK;QACX,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,MAAM,EAAE,oBAAoB,CAAC,MAAM;QACnC,iEAAiE;QACjE,oEAAoE;QACpE,+DAA+D;QAC/D,kEAAkE;QAClE,mEAAmE;QACnE,kEAAkE;QAClE,SAAS,EAAE,EAAE,eAAe,EAAE,oBAAoB,EAAE,eAAe,EAAE;KACrE,CAAC;IAEF,iEAAiE;IACjE,+BAA+B;IAC/B,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,EAAE;QACjC,GAAG;YACF,OAAO,GAAG,CAAC,OAAO,CAAC;QACpB,CAAC;QACD,UAAU,EAAE,IAAI;KAChB,CAAC,CAAC;IACH,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE;QAClC,GAAG;YACF,OAAO,GAAG,CAAC,QAAQ,CAAC;QACrB,CAAC;QACD,UAAU,EAAE,IAAI;KAChB,CAAC,CAAC;IAEH,kEAAkE;IAClE,6DAA6D;IAC7D,gEAAgE;IAChE,iCAAiC;IACjC,GAAG,CAAC,OAAO,GAAG,CAAC,IAAY,EAAE,KAAc,EAAQ,EAAE;QACnD,GAAG,CAAC,KAAiC,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACtD,CAAC,CAAC;IAEF,OAAO,GAAG,CAAC;AACZ,CAAC"}
@@ -1,44 +1,9 @@
1
- /**
2
- * WorkflowNormalizer — accepts v1 or v2 workflow shapes and projects both
3
- * to the single canonical internal shape that `Configuration.getSteps` /
4
- * `Configuration.getNodes` already consume.
5
- *
6
- * **Input shapes accepted**
7
- *
8
- * v1 (legacy):
9
- * ```
10
- * {
11
- * name, version, trigger,
12
- * steps: [{ name, node, type, active?, stop?, set_var? }],
13
- * nodes: { [stepName]: { inputs?, conditions? } }
14
- * }
15
- * ```
16
- *
17
- * v2 (canonical):
18
- * ```
19
- * {
20
- * name, version, trigger,
21
- * steps: [
22
- * { id, use, type?, inputs?, as?, spread?, ephemeral?, active?, stop? }
23
- * | { id, branch: { when, then, else? } }
24
- * ]
25
- * }
26
- * ```
27
- *
28
- * **Output shape** (always v1-compatible internal):
29
- * ```
30
- * {
31
- * name, version, trigger, // method "*" normalized to "ANY"
32
- * steps: [{ name, node, type, active, stop, set_var, as, spread, ephemeral, ... }],
33
- * nodes: { [stepName]: { inputs?, conditions? } }
34
- * }
35
- * ```
36
- *
37
- * **Why a single internal shape?** The runner core (RunnerSteps,
38
- * Configuration, Blok.run, etc.) is unchanged. Normalization is purely
39
- * an authoring-layer concern. Old workflows keep running; new authoring
40
- * shapes get translated transparently.
41
- */
1
+ interface RetryConfig {
2
+ maxAttempts: number;
3
+ minTimeoutInMs?: number;
4
+ maxTimeoutInMs?: number;
5
+ factor?: number;
6
+ }
42
7
  interface InternalStep {
43
8
  name: string;
44
9
  node: string;
@@ -51,6 +16,29 @@ interface InternalStep {
51
16
  ephemeral?: boolean;
52
17
  stream_logs?: boolean;
53
18
  flow?: boolean;
19
+ idempotencyKey?: string;
20
+ idempotencyKeyTTL?: number;
21
+ retry?: RetryConfig;
22
+ subworkflow?: string;
23
+ wait?: boolean;
24
+ /**
25
+ * Tier 2 quick-wins — per-attempt execution timeout. Number (ms) or
26
+ * duration string (`"30s"`, etc.). Configuration thread-through
27
+ * normalizes to milliseconds via `parseDuration`.
28
+ */
29
+ maxDuration?: number | string;
30
+ /**
31
+ * PR 4 — `wait.for(duration)` / `wait.until(date)` step.
32
+ *
33
+ * Discriminates by `type === "wait"` and the presence of either
34
+ * `waitForMs` (numeric ms after parseDuration) or `waitUntil` (number
35
+ * ms-since-epoch OR string for $-proxy / ISO).
36
+ *
37
+ * `wait?: boolean` above is the sub-workflow `wait: true|false` flag
38
+ * — separate concern, separate field.
39
+ */
40
+ waitForMs?: number;
41
+ waitUntil?: number | string;
54
42
  [key: string]: unknown;
55
43
  }
56
44
  interface InternalNodeConfig {