@blokjs/runner 0.2.2 → 0.6.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 (213) hide show
  1. package/dist/Blok.js +32 -3
  2. package/dist/Blok.js.map +1 -1
  3. package/dist/Configuration.d.ts +59 -5
  4. package/dist/Configuration.js +366 -96
  5. package/dist/Configuration.js.map +1 -1
  6. package/dist/ForEachNode.d.ts +59 -0
  7. package/dist/ForEachNode.js +522 -0
  8. package/dist/ForEachNode.js.map +1 -0
  9. package/dist/LoopMaxIterationsError.d.ts +11 -0
  10. package/dist/LoopMaxIterationsError.js +18 -0
  11. package/dist/LoopMaxIterationsError.js.map +1 -0
  12. package/dist/LoopNode.d.ts +36 -0
  13. package/dist/LoopNode.js +182 -0
  14. package/dist/LoopNode.js.map +1 -0
  15. package/dist/PayloadTooLargeError.d.ts +19 -0
  16. package/dist/PayloadTooLargeError.js +29 -0
  17. package/dist/PayloadTooLargeError.js.map +1 -0
  18. package/dist/RunCancelledError.d.ts +17 -0
  19. package/dist/RunCancelledError.js +25 -0
  20. package/dist/RunCancelledError.js.map +1 -0
  21. package/dist/Runner.d.ts +11 -1
  22. package/dist/Runner.js +9 -2
  23. package/dist/Runner.js.map +1 -1
  24. package/dist/RunnerSteps.js +648 -44
  25. package/dist/RunnerSteps.js.map +1 -1
  26. package/dist/RuntimeAdapterNode.d.ts +2 -1
  27. package/dist/RuntimeAdapterNode.js +2 -2
  28. package/dist/RuntimeAdapterNode.js.map +1 -1
  29. package/dist/RuntimeRegistry.d.ts +23 -2
  30. package/dist/RuntimeRegistry.js +31 -2
  31. package/dist/RuntimeRegistry.js.map +1 -1
  32. package/dist/SubworkflowNode.d.ts +181 -0
  33. package/dist/SubworkflowNode.js +479 -0
  34. package/dist/SubworkflowNode.js.map +1 -0
  35. package/dist/SwitchNode.d.ts +37 -0
  36. package/dist/SwitchNode.js +153 -0
  37. package/dist/SwitchNode.js.map +1 -0
  38. package/dist/TriggerBase.d.ts +178 -0
  39. package/dist/TriggerBase.js +1032 -5
  40. package/dist/TriggerBase.js.map +1 -1
  41. package/dist/TryCatchNode.d.ts +32 -0
  42. package/dist/TryCatchNode.js +207 -0
  43. package/dist/TryCatchNode.js.map +1 -0
  44. package/dist/WaitDispatchRequest.d.ts +38 -0
  45. package/dist/WaitDispatchRequest.js +13 -0
  46. package/dist/WaitDispatchRequest.js.map +1 -0
  47. package/dist/WaitNode.d.ts +23 -0
  48. package/dist/WaitNode.js +26 -0
  49. package/dist/WaitNode.js.map +1 -0
  50. package/dist/adapters/grpc/GrpcCodec.js +2 -2
  51. package/dist/adapters/grpc/GrpcRuntimeAdapter.d.ts +6 -4
  52. package/dist/adapters/grpc/GrpcRuntimeAdapter.js +6 -4
  53. package/dist/adapters/grpc/GrpcRuntimeAdapter.js.map +1 -1
  54. package/dist/adapters/grpc/types.d.ts +7 -5
  55. package/dist/adapters/grpc/types.js.map +1 -1
  56. package/dist/adapters/transport.d.ts +12 -41
  57. package/dist/adapters/transport.js +21 -70
  58. package/dist/adapters/transport.js.map +1 -1
  59. package/dist/cache/NodeResultCache.js +7 -0
  60. package/dist/cache/NodeResultCache.js.map +1 -1
  61. package/dist/concurrency/ConcurrencyBackend.d.ts +61 -0
  62. package/dist/concurrency/ConcurrencyBackend.js +20 -0
  63. package/dist/concurrency/ConcurrencyBackend.js.map +1 -0
  64. package/dist/concurrency/ConcurrencyLimitError.d.ts +37 -0
  65. package/dist/concurrency/ConcurrencyLimitError.js +16 -0
  66. package/dist/concurrency/ConcurrencyLimitError.js.map +1 -0
  67. package/dist/concurrency/NatsKvConcurrencyBackend.d.ts +64 -0
  68. package/dist/concurrency/NatsKvConcurrencyBackend.js +310 -0
  69. package/dist/concurrency/NatsKvConcurrencyBackend.js.map +1 -0
  70. package/dist/concurrency/QueueExpiredError.d.ts +40 -0
  71. package/dist/concurrency/QueueExpiredError.js +15 -0
  72. package/dist/concurrency/QueueExpiredError.js.map +1 -0
  73. package/dist/concurrency/RedisConcurrencyBackend.d.ts +64 -0
  74. package/dist/concurrency/RedisConcurrencyBackend.js +374 -0
  75. package/dist/concurrency/RedisConcurrencyBackend.js.map +1 -0
  76. package/dist/concurrency/createConcurrencyBackend.d.ts +24 -0
  77. package/dist/concurrency/createConcurrencyBackend.js +38 -0
  78. package/dist/concurrency/createConcurrencyBackend.js.map +1 -0
  79. package/dist/concurrency/readConcurrencyConfig.d.ts +60 -0
  80. package/dist/concurrency/readConcurrencyConfig.js +60 -0
  81. package/dist/concurrency/readConcurrencyConfig.js.map +1 -0
  82. package/dist/defineNode.d.ts +8 -0
  83. package/dist/defineNode.js +25 -5
  84. package/dist/defineNode.js.map +1 -1
  85. package/dist/graphql/GraphQLSchemaGenerator.js +1 -1
  86. package/dist/graphql/GraphQLSchemaGenerator.js.map +1 -1
  87. package/dist/idempotency/resolveIdempotencyKey.d.ts +20 -0
  88. package/dist/idempotency/resolveIdempotencyKey.js +37 -0
  89. package/dist/idempotency/resolveIdempotencyKey.js.map +1 -0
  90. package/dist/index.d.ts +30 -6
  91. package/dist/index.js +55 -6
  92. package/dist/index.js.map +1 -1
  93. package/dist/marketplace/RuntimeCatalog.d.ts +6 -0
  94. package/dist/marketplace/RuntimeCatalog.js.map +1 -1
  95. package/dist/marketplace/RuntimeDiscovery.d.ts +2 -2
  96. package/dist/marketplace/RuntimeDiscovery.js +18 -6
  97. package/dist/marketplace/RuntimeDiscovery.js.map +1 -1
  98. package/dist/monitoring/ConcurrencyMetrics.d.ts +82 -0
  99. package/dist/monitoring/ConcurrencyMetrics.js +139 -0
  100. package/dist/monitoring/ConcurrencyMetrics.js.map +1 -0
  101. package/dist/monitoring/ForEachWaitMetrics.d.ts +22 -0
  102. package/dist/monitoring/ForEachWaitMetrics.js +36 -0
  103. package/dist/monitoring/ForEachWaitMetrics.js.map +1 -0
  104. package/dist/monitoring/JanitorMetrics.d.ts +27 -0
  105. package/dist/monitoring/JanitorMetrics.js +48 -0
  106. package/dist/monitoring/JanitorMetrics.js.map +1 -0
  107. package/dist/openapi/OpenAPIGenerator.js +7 -2
  108. package/dist/openapi/OpenAPIGenerator.js.map +1 -1
  109. package/dist/runtime/PrimitiveStack.d.ts +64 -0
  110. package/dist/runtime/PrimitiveStack.js +92 -0
  111. package/dist/runtime/PrimitiveStack.js.map +1 -0
  112. package/dist/scheduling/DebounceBackend.d.ts +108 -0
  113. package/dist/scheduling/DebounceBackend.js +23 -0
  114. package/dist/scheduling/DebounceBackend.js.map +1 -0
  115. package/dist/scheduling/DebounceCoordinator.d.ts +141 -0
  116. package/dist/scheduling/DebounceCoordinator.js +362 -0
  117. package/dist/scheduling/DebounceCoordinator.js.map +1 -0
  118. package/dist/scheduling/DeferredDispatchSignal.d.ts +50 -0
  119. package/dist/scheduling/DeferredDispatchSignal.js +14 -0
  120. package/dist/scheduling/DeferredDispatchSignal.js.map +1 -0
  121. package/dist/scheduling/DeferredRunScheduler.d.ts +96 -0
  122. package/dist/scheduling/DeferredRunScheduler.js +256 -0
  123. package/dist/scheduling/DeferredRunScheduler.js.map +1 -0
  124. package/dist/scheduling/NatsKvDebounceBackend.d.ts +53 -0
  125. package/dist/scheduling/NatsKvDebounceBackend.js +334 -0
  126. package/dist/scheduling/NatsKvDebounceBackend.js.map +1 -0
  127. package/dist/scheduling/RedisDebounceBackend.d.ts +49 -0
  128. package/dist/scheduling/RedisDebounceBackend.js +356 -0
  129. package/dist/scheduling/RedisDebounceBackend.js.map +1 -0
  130. package/dist/scheduling/createDebounceBackend.d.ts +25 -0
  131. package/dist/scheduling/createDebounceBackend.js +39 -0
  132. package/dist/scheduling/createDebounceBackend.js.map +1 -0
  133. package/dist/scheduling/readSchedulingConfig.d.ts +24 -0
  134. package/dist/scheduling/readSchedulingConfig.js +52 -0
  135. package/dist/scheduling/readSchedulingConfig.js.map +1 -0
  136. package/dist/security/AuditLogger.js +1 -1
  137. package/dist/security/AuditLogger.js.map +1 -1
  138. package/dist/security/AuthMiddleware.d.ts +19 -20
  139. package/dist/security/AuthMiddleware.js +35 -20
  140. package/dist/security/AuthMiddleware.js.map +1 -1
  141. package/dist/security/OAuthProvider.js +2 -2
  142. package/dist/security/OAuthProvider.js.map +1 -1
  143. package/dist/security/SecretManager.js +14 -13
  144. package/dist/security/SecretManager.js.map +1 -1
  145. package/dist/security/index.d.ts +3 -1
  146. package/dist/security/index.js +3 -1
  147. package/dist/security/index.js.map +1 -1
  148. package/dist/testing/TestHarness.d.ts +27 -12
  149. package/dist/testing/TestHarness.js +19 -3
  150. package/dist/testing/TestHarness.js.map +1 -1
  151. package/dist/testing/WorkflowTestRunner.js +0 -7
  152. package/dist/testing/WorkflowTestRunner.js.map +1 -1
  153. package/dist/timeouts/StepTimeoutError.d.ts +22 -0
  154. package/dist/timeouts/StepTimeoutError.js +31 -0
  155. package/dist/timeouts/StepTimeoutError.js.map +1 -0
  156. package/dist/tracing/InMemoryRunStore.d.ts +41 -1
  157. package/dist/tracing/InMemoryRunStore.js +239 -0
  158. package/dist/tracing/InMemoryRunStore.js.map +1 -1
  159. package/dist/tracing/Janitor.d.ts +70 -0
  160. package/dist/tracing/Janitor.js +150 -0
  161. package/dist/tracing/Janitor.js.map +1 -0
  162. package/dist/tracing/PostgresRunStore.d.ts +57 -1
  163. package/dist/tracing/PostgresRunStore.js +711 -6
  164. package/dist/tracing/PostgresRunStore.js.map +1 -1
  165. package/dist/tracing/RoutingDiagnostics.d.ts +55 -0
  166. package/dist/tracing/RoutingDiagnostics.js +50 -0
  167. package/dist/tracing/RoutingDiagnostics.js.map +1 -0
  168. package/dist/tracing/RunStore.d.ts +181 -1
  169. package/dist/tracing/RunTracker.d.ts +244 -9
  170. package/dist/tracing/RunTracker.js +594 -1
  171. package/dist/tracing/RunTracker.js.map +1 -1
  172. package/dist/tracing/SqliteRunStore.d.ts +79 -2
  173. package/dist/tracing/SqliteRunStore.js +775 -16
  174. package/dist/tracing/SqliteRunStore.js.map +1 -1
  175. package/dist/tracing/TraceRouter.d.ts +20 -2
  176. package/dist/tracing/TraceRouter.js +612 -6
  177. package/dist/tracing/TraceRouter.js.map +1 -1
  178. package/dist/tracing/createStore.js +14 -3
  179. package/dist/tracing/createStore.js.map +1 -1
  180. package/dist/tracing/metadataFilter.d.ts +63 -0
  181. package/dist/tracing/metadataFilter.js +224 -0
  182. package/dist/tracing/metadataFilter.js.map +1 -0
  183. package/dist/tracing/sanitize.d.ts +11 -0
  184. package/dist/tracing/sanitize.js +29 -0
  185. package/dist/tracing/sanitize.js.map +1 -1
  186. package/dist/tracing/types.d.ts +672 -2
  187. package/dist/utils/createChildContext.d.ts +32 -0
  188. package/dist/utils/createChildContext.js +113 -0
  189. package/dist/utils/createChildContext.js.map +1 -0
  190. package/dist/utils/envAllowlist.d.ts +35 -0
  191. package/dist/utils/envAllowlist.js +113 -0
  192. package/dist/utils/envAllowlist.js.map +1 -0
  193. package/dist/version/RuntimeVersionValidator.d.ts +38 -0
  194. package/dist/version/RuntimeVersionValidator.js +121 -0
  195. package/dist/version/RuntimeVersionValidator.js.map +1 -0
  196. package/dist/visualization/WorkflowVisualizer.js +4 -4
  197. package/dist/visualization/WorkflowVisualizer.js.map +1 -1
  198. package/dist/workflow/PersistenceHelper.d.ts +18 -10
  199. package/dist/workflow/PersistenceHelper.js +35 -9
  200. package/dist/workflow/PersistenceHelper.js.map +1 -1
  201. package/dist/workflow/WorkflowNormalizer.d.ts +48 -42
  202. package/dist/workflow/WorkflowNormalizer.js +650 -18
  203. package/dist/workflow/WorkflowNormalizer.js.map +1 -1
  204. package/dist/workflow/WorkflowRegistry.d.ts +186 -0
  205. package/dist/workflow/WorkflowRegistry.js +202 -0
  206. package/dist/workflow/WorkflowRegistry.js.map +1 -0
  207. package/dist/workflow/sampleBody.d.ts +54 -0
  208. package/dist/workflow/sampleBody.js +320 -0
  209. package/dist/workflow/sampleBody.js.map +1 -0
  210. package/package.json +3 -8
  211. package/dist/adapters/HttpRuntimeAdapter.d.ts +0 -79
  212. package/dist/adapters/HttpRuntimeAdapter.js +0 -233
  213. package/dist/adapters/HttpRuntimeAdapter.js.map +0 -1
@@ -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,107 @@ 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;
164
+ /**
165
+ * v0.6 prerequisite for wait-inside-primitives Phase 2.
166
+ *
167
+ * JSON-serialized snapshot of `ctx.state` taken immediately before
168
+ * `RunnerSteps` throws `WaitDispatchRequest`. On `dispatchDeferred`
169
+ * re-entry — including the cross-process recovery path where the
170
+ * scheduler re-builds a fresh ctx from the persisted dispatch row —
171
+ * `TriggerBase.run` rehydrates `ctx.state` (and its `ctx.vars`
172
+ * alias) from this column so subsequent steps see the same
173
+ * pre-wait state regardless of process restart.
174
+ *
175
+ * Without this column, only the in-process timer-fire path would
176
+ * preserve state (state still lives on the original ctx). The
177
+ * cross-process path would resume with empty `ctx.state`, breaking
178
+ * Phase 2's iteration-state-persistence promise (a forEach
179
+ * iteration whose body fired the wait would lose its index, the
180
+ * loop's accumulator, etc.).
181
+ *
182
+ * Capped by `BLOK_STATE_SNAPSHOT_MAX_BYTES` (default 1MB,
183
+ * matching `BLOK_DISPATCH_PAYLOAD_MAX_BYTES`) — when the
184
+ * serialized state exceeds the cap, the runner logs a warning and
185
+ * skips the snapshot (the wait still defers; resumption is
186
+ * best-effort). Authors can opt out entirely via
187
+ * `BLOK_STATE_SNAPSHOT_DISABLED=1`. Sensitive keys aren't filtered
188
+ * here — the snapshot only persists what the workflow has already
189
+ * computed into `state`, so apply your own redaction in nodes that
190
+ * write secrets there.
191
+ *
192
+ * Undefined for runs that never threw `WaitDispatchRequest`, which
193
+ * is the vast majority. Persisted column is `state_snapshot`
194
+ * (sqlite migration v11).
195
+ */
196
+ stateSnapshot?: string;
63
197
  }
64
198
  export type NodeRunStatus = "pending" | "running" | "completed" | "failed" | "skipped";
65
199
  export interface NodeRun {
@@ -113,12 +247,369 @@ export interface NodeRun {
113
247
  /** Bytes received from the SDK in the response. */
114
248
  response_bytes?: number;
115
249
  };
250
+ /**
251
+ * Tier 1 idempotency cache lineage. Populated by `RunTracker.markNodeCached`
252
+ * when the step short-circuited via a cache hit instead of running. Studio
253
+ * renders a "CACHED" badge with a click-through to the source run/node.
254
+ * Absent on regular completed nodes.
255
+ */
256
+ cached?: {
257
+ sourceRunId: string;
258
+ sourceNodeRunId: string;
259
+ cachedAt: number;
260
+ };
261
+ /**
262
+ * Tier 1 retry attempts. One entry per `NODE_ATTEMPT_FAILED` event the
263
+ * step emitted before either succeeding or exhausting `retry.maxAttempts`.
264
+ * Capped at 10 entries (`MAX_STORED_ATTEMPTS`) — extreme retry counts
265
+ * are bounded so a runaway loop can't bloat the run store.
266
+ *
267
+ * Studio renders this as a collapsible "Attempts (N)" disclosure on the
268
+ * step's panel. The final outcome lives on the node's status / error
269
+ * fields; this array carries only the failed attempts that came before.
270
+ */
271
+ attempts?: Array<{
272
+ attempt: number;
273
+ error: RunErrorDetail;
274
+ timestamp: number;
275
+ }>;
276
+ /**
277
+ * Tier 2 #4 sub-workflow mode — only set for `nodeType === "subworkflow"`.
278
+ * - `true` (default) — synchronous: parent step blocks on child completion.
279
+ * - `false` — fire-and-forget: parent returns dispatch metadata immediately;
280
+ * child runs asynchronously via `setImmediate`. Studio renders a distinct
281
+ * `↳ async` badge in StepRail to differentiate from synchronous siblings.
282
+ *
283
+ * Captured at `tracker.startNode()` time so it survives the trace and is
284
+ * visible to Studio without recomputing from outputs heuristics.
285
+ */
286
+ wait?: boolean;
287
+ /**
288
+ * G2 (v0.6) sub-workflow dispatch strategy — only set for
289
+ * `nodeType === "subworkflow"`. Mirrors the step config's `dispatch`
290
+ * field so Studio can distinguish in-process invocations from HTTP
291
+ * self-call dispatches at a glance.
292
+ *
293
+ * - `"in-process"` (default; also represented by `undefined` on legacy
294
+ * traces) — child runs in the same Node process via the in-process
295
+ * `Runner`.
296
+ * - `"http-self"` — child is dispatched as a fresh HTTP request to
297
+ * `BLOK_SELF_BASE_URL`, potentially landing on a different process
298
+ * in a horizontally-scaled deployment.
299
+ *
300
+ * Captured at `tracker.startNode()` time alongside `wait` so the rail
301
+ * badge can compose them (`↳ async` orange + `http` sky for an
302
+ * async http-self dispatch).
303
+ */
304
+ dispatch?: "in-process" | "http-self";
305
+ /**
306
+ * PR 5 E3 — sub-workflow nesting depth surfaced for Studio.
307
+ *
308
+ * Set on subworkflow node runs. Top-level workflow's sub-workflow
309
+ * step has `subworkflowDepth = 1`; that child's sub-workflow step
310
+ * has `subworkflowDepth = 2`; etc. Bounded by
311
+ * `BLOK_MAX_SUBWORKFLOW_DEPTH` (default 10).
312
+ *
313
+ * Studio renders `↳ async (N)` / `↳ sub (N)` only when N >= 2 to
314
+ * keep top-level invocations un-cluttered.
315
+ */
316
+ subworkflowDepth?: number;
317
+ /**
318
+ * v0.5 — origin middleware name when the trigger's
319
+ * `runMiddlewareChain` produced this NodeRun. The trigger sets a
320
+ * `_blokMiddlewareName` sentinel on ctx before dispatching each
321
+ * middleware; RunnerSteps reads it and propagates here so Studio's
322
+ * StepRail can surface a `mw:<name>` badge on the inner steps a
323
+ * middleware produced (otherwise indistinguishable from regular
324
+ * depth-1 nested steps).
325
+ */
326
+ middleware?: string;
327
+ /**
328
+ * v0.5.3 — iteration index for steps inside a `forEach` or `loop`
329
+ * primitive. Set per-iteration by ForEachNode / LoopNode on the
330
+ * cloned child ctx (`_blokIterationIndex` sentinel); RunnerSteps
331
+ * reads it and propagates here. Studio's StepRail groups consecutive
332
+ * sibling rows that share an iterationIndex into a collapsible
333
+ * "iteration N" header so a 5-iteration forEach with 3 inner steps
334
+ * renders as 5 grouped sections instead of 15 flat rows with
335
+ * duplicate names. Undefined for top-level steps, for steps inside
336
+ * non-iteration primitives (`tryCatch`, `switch`), and for legacy
337
+ * traces written before v0.5.3.
338
+ */
339
+ iterationIndex?: number;
340
+ /**
341
+ * v0.6 wait-inside-primitives — iteration cursor. Set on a primitive
342
+ * iterator's NodeRun (e.g. forEach, loop) by RunnerSteps when an
343
+ * INNER step throws `WaitDispatchRequest` mid-iteration. Read by
344
+ * `TriggerBase.run` on dispatchDeferred re-entry and stamped onto
345
+ * `ctx._blokIterationResume` so the primitive can pick it up.
346
+ * Persisted column is `iteration_context` (sqlite migration v12).
347
+ *
348
+ * Two encodings via a `mode` discriminator:
349
+ *
350
+ * - `mode: "sequential"` (or omitted for back-compat): the Phase 2
351
+ * sequential forEach + wait cursor (also reused by Phase 3 loop).
352
+ * Records the in-flight iteration index, the inner step index
353
+ * where the wait fired, and an in-order `completedResults`
354
+ * accumulator covering iterations `[0..iteration-1]`. ForEachNode
355
+ * and LoopNode read this on resume to skip the completed prefix.
356
+ *
357
+ * - `mode: "parallel"` (PR follow-up to Phase 3): the parallel
358
+ * forEach + wait cursor. Records which iteration's wait fired
359
+ * (`waitFiringIteration`), where inside its body
360
+ * (`innerStepIndex`), a SPARSE `completedResults` array (slot `i`
361
+ * populated iff iteration `i` finished before the wait fired —
362
+ * `null` for "ran but returned undefined", JSON-undefined hole
363
+ * for "not present"), and the explicit `cancelledIterations`
364
+ * list (in-flight + queued indices that need re-launch on
365
+ * resume). See `docs/c/devtools/parallel-foreach-wait-spec.mdx`
366
+ * for the full contract.
367
+ *
368
+ * Undefined on regular NodeRuns and on primitives that completed
369
+ * without ever throwing a wait.
370
+ */
371
+ iterationContext?: IterationContext;
372
+ }
373
+ /**
374
+ * Discriminated union for the iteration cursor persisted into
375
+ * `node_runs.iteration_context`. See `NodeRun.iterationContext` for
376
+ * field-level semantics. The runtime selection lives in:
377
+ *
378
+ * - Write side: `core/runner/src/RunnerSteps.ts` wait-throw site
379
+ * (sequential) and `ForEachNode.run` parallel-branch error handler
380
+ * (parallel).
381
+ * - Read side: `TriggerBase.run` rehydrate stamps the cursor onto
382
+ * `ctx._blokIterationResume`; the primitive itself (ForEachNode,
383
+ * LoopNode) dispatches on `mode` to the right resume path.
384
+ */
385
+ export type IterationContext = SequentialIterationContext | ParallelIterationContext | SwitchIterationContext;
386
+ /**
387
+ * Sequential forEach + wait OR loop + wait cursor. The pre-existing
388
+ * Phase 2/3 shape — `mode` is optional and defaults to "sequential" on
389
+ * read so cursors written before the discriminator landed (i.e., main
390
+ * before this PR) keep being interpreted correctly.
391
+ */
392
+ export interface SequentialIterationContext {
393
+ mode?: "sequential";
394
+ /** Iteration index (0-based) the wait fired in. */
395
+ iteration: number;
396
+ /** Inner step index within iteration's body where the wait fired. */
397
+ innerStepIndex: number;
398
+ /** Accumulator for iterations [0..iteration-1]; pre-populates `results[]` on resume. */
399
+ completedResults: unknown[];
400
+ }
401
+ /**
402
+ * v0.6 Phase 4 — switch + wait cursor. Records which case arm matched
403
+ * (so on re-entry SwitchNode walks back into the same arm) plus the
404
+ * inner step within that arm where the wait fired.
405
+ *
406
+ * `caseIndex` semantics:
407
+ * - `>= 0` — index into the workflow's `cases[]` array.
408
+ * - `-1` — the `default` arm matched.
409
+ *
410
+ * `completedResults` is unused for switch (it doesn't iterate) but the
411
+ * field is kept on the IterationContext union for cross-schema parity.
412
+ * Always `[]` on switch.
413
+ */
414
+ export interface SwitchIterationContext {
415
+ mode: "switch";
416
+ /** Resolved case index; `-1` denotes the `default` arm. */
417
+ caseIndex: number;
418
+ /** Inner step index within the matched arm where the wait fired. */
419
+ innerStepIndex: number;
420
+ /** Unused for switch; preserved at schema level for parity. Always `[]`. */
421
+ completedResults: never[];
422
+ }
423
+ /**
424
+ * Parallel forEach + wait cursor. The headline contract: persist
425
+ * completed iteration results, retry incomplete ones on resume. See
426
+ * `docs/c/devtools/parallel-foreach-wait-spec.mdx` for the full design.
427
+ */
428
+ export interface ParallelIterationContext {
429
+ mode: "parallel";
430
+ /** Index of the iteration whose inner step threw the wait. */
431
+ waitFiringIteration: number;
432
+ /** Inner step index within the wait-firing iteration's body. */
433
+ innerStepIndex: number;
434
+ /**
435
+ * Sparse array indexed by iteration number. Slot `i` is populated
436
+ * iff iteration `i` finished before the wait fired. `null` means
437
+ * "ran but returned undefined" (distinct from the JSON-undefined
438
+ * hole = "not present in cursor, re-launch on resume").
439
+ */
440
+ completedResults: (unknown | null)[];
441
+ /**
442
+ * Iteration indices that were in-flight OR queued at wait-throw
443
+ * moment. ForEachNode re-launches these from inner step 0 on
444
+ * resume. The wait-firing iteration is NOT in this list — it
445
+ * resumes via `innerStepIndex`. Sorted ascending for trace-dump
446
+ * readability.
447
+ */
448
+ cancelledIterations: number[];
449
+ }
450
+ /**
451
+ * Stored result of a previously-successful step execution, keyed by
452
+ * `(workflowName, step.id, idempotencyKey)`. On cache hit, RunnerSteps
453
+ * skips `step.process()` and replays the cached `data` through the same
454
+ * `PersistenceHelper.applyStepOutput` rules — caching layers ABOVE
455
+ * persistence, never within it.
456
+ */
457
+ export interface CachedStepResult {
458
+ /** The data the step originally returned. */
459
+ data: unknown;
460
+ /** ms since epoch when the entry was written. */
461
+ cachedAt: number;
462
+ /** ms since epoch when the entry expires; null = no expiry. */
463
+ expiresAt: number | null;
464
+ /** Run that originally produced this result. */
465
+ sourceRunId: string;
466
+ /** Node run that originally produced this result. */
467
+ sourceNodeRunId: string;
468
+ }
469
+ /**
470
+ * Outcome of an `acquireConcurrencySlot` attempt (Tier 2 #6).
471
+ *
472
+ * The store's gate decides whether the run is allowed to proceed by
473
+ * comparing the current in-flight count for the (workflow, key) pair
474
+ * against the requested limit.
475
+ */
476
+ export interface ConcurrencySlotResult {
477
+ /** True when the slot was granted; false when the run should be throttled. */
478
+ acquired: boolean;
479
+ /**
480
+ * Number of in-flight runs (including the just-acquired one when
481
+ * `acquired === true`) sharing the same (workflowName, concurrencyKey).
482
+ * Useful for observability — Studio surfaces this on `RUN_THROTTLED`.
483
+ */
484
+ currentInFlight: number;
485
+ }
486
+ /**
487
+ * A persisted scheduled dispatch — one row per pending HTTP-trigger
488
+ * deferral. Written by `DeferredRunScheduler.schedule()` when a
489
+ * `persist` payload is provided; deleted on cancel or fire.
490
+ *
491
+ * Boot recovery (HttpTrigger.recoverDispatches) scans this table,
492
+ * marks past-due+TTL-expired rows as `"expired"`, and re-registers
493
+ * timers for live dispatches.
494
+ */
495
+ export interface ScheduledDispatchRow {
496
+ runId: string;
497
+ workflowName: string;
498
+ /** `"http"` for v1; future triggers can opt in. */
499
+ triggerType: string;
500
+ /** ms since epoch when to dispatch. */
501
+ scheduledAt: number;
502
+ /** ms since epoch TTL deadline (undefined = no TTL). */
503
+ expiresAt?: number;
504
+ /** Mirrors the run record's status — `"delayed" | "queued" | "debounced"`. */
505
+ dispatchStatus: "delayed" | "queued" | "debounced";
506
+ /**
507
+ * JSON-serialized minimal Context subset, trigger-defined.
508
+ * For HTTP: `{method, path, headers, body, params, query, workflowPath}`
509
+ * with sensitive header keys stripped (authorization, cookie, x-api-key).
510
+ */
511
+ payload: unknown;
512
+ /** ms since epoch when the row was first written. */
513
+ createdAt: number;
514
+ /**
515
+ * Tier C #2 — cross-process scheduler coordination. Set when a
516
+ * process claims this dispatch during boot recovery. Unset on
517
+ * unclaimed rows.
518
+ *
519
+ * Multi-process PG deployments use the claim to prevent the same
520
+ * dispatch from being fired by two processes that both ran
521
+ * `recoverDispatches()` against the shared table. Single-process
522
+ * and per-process sqlite deployments see no observable difference
523
+ * (the same process always wins the claim).
524
+ */
525
+ claimedBy?: string;
526
+ /**
527
+ * Tier C #2 — last heartbeat timestamp (ms since epoch). The
528
+ * holder of `claimedBy` periodically refreshes `claimedAt` while
529
+ * the timer is registered. Stale claims (now > claimedAt + leaseMs)
530
+ * are eligible for takeover by another process — protects against
531
+ * crashed holders blocking recovery indefinitely.
532
+ */
533
+ claimedAt?: number;
116
534
  }
117
535
  export type RunEventType = "RUN_STARTED" | "RUN_COMPLETED" | "RUN_FAILED" | "NODE_STARTED" | "NODE_COMPLETED" | "NODE_FAILED" | "NODE_SKIPPED" | "VARS_UPDATED" | "LOG_ENTRY"
118
536
  /** §17 Phase 5: streaming `Progress` frame from the SDK. */
119
537
  | "NODE_PROGRESS"
120
538
  /** §17 Phase 5: streaming `PartialResult` frame from the SDK. */
121
- | "NODE_PARTIAL_RESULT";
539
+ | "NODE_PARTIAL_RESULT"
540
+ /**
541
+ * Tier 1 idempotency cache hit: the step short-circuited via the
542
+ * idempotency cache instead of running. Payload carries
543
+ * `{ durationMs, source: { sourceRunId, sourceNodeRunId, cachedAt } }`.
544
+ * Studio renders a CACHED badge on the affected node.
545
+ */
546
+ | "NODE_CACHED"
547
+ /**
548
+ * Tier 1 retry: a step attempt failed and another retry will follow.
549
+ * Payload carries `{ attempt, error }`. Final attempt failure (after
550
+ * exhausting `retry.maxAttempts`) emits `NODE_FAILED` instead.
551
+ */
552
+ | "NODE_ATTEMPT_FAILED"
553
+ /**
554
+ * Tier 2 #6 concurrency gate denied a run before any step executed.
555
+ * Payload carries `{ concurrencyKey, concurrencyLimit, currentInFlight }`.
556
+ * The run's status flips to `"throttled"` (distinct from `"failed"` —
557
+ * no step ran). Studio surfaces a Throttled badge.
558
+ */
559
+ | "RUN_THROTTLED"
560
+ /**
561
+ * Tier 2 #5 — run scheduled for later dispatch via `trigger.delay`.
562
+ * Payload `{scheduledAt, delayMs, expiresAt?}`. Status flips to
563
+ * `"delayed"`; transitions to `"running"` when the timer fires.
564
+ */
565
+ | "RUN_DELAYED"
566
+ /**
567
+ * Tier 2 #5 — TTL exceeded; run auto-cancelled before dispatch.
568
+ * Payload `{expiresAt, expiredAt, lateBy}`. Status flips to `"expired"`.
569
+ */
570
+ | "RUN_EXPIRED"
571
+ /**
572
+ * Tier 2 #7 — run coalesced into a debounce window. Payload
573
+ * `{debounceKey, mode, intoRunId?, pingCount?}`. Status flips to
574
+ * `"debounced"`. In leading mode this is terminal (the run never
575
+ * executes — execution went to a sibling). In trailing mode this is
576
+ * transient and the same run flips to `"running"` when the window
577
+ * closes.
578
+ */
579
+ | "RUN_DEBOUNCED"
580
+ /**
581
+ * Tier 2 #6 follow-up — concurrency gate denied a run AND the trigger
582
+ * is configured with `onLimit: "queue"`. Instead of throwing, the run
583
+ * is deferred via `DeferredRunScheduler` and re-attempts acquisition
584
+ * after a 1s delay. Payload `{concurrencyKey, concurrencyLimit,
585
+ * currentInFlight, scheduledAt}`. Status flips to `"queued"`;
586
+ * transitions to `"running"` when the timer fires (and may flip back
587
+ * to `"queued"` on re-denial).
588
+ */
589
+ | "RUN_QUEUED"
590
+ /**
591
+ * Tier 2 polish — operator cancelled a pending (delayed/debounced/
592
+ * queued) run via `POST /__blok/runs/:runId/cancel`. Payload
593
+ * `{durationMs, previousStatus}`. Status flips to `"cancelled"`.
594
+ * Currently only pre-execution states are cancellable; running runs
595
+ * require cooperative `AbortSignal` (deferred follow-up).
596
+ */
597
+ | "RUN_CANCELLED"
598
+ /**
599
+ * Tier 2 quick-wins — run crashed (uncaught exception / OOM /
600
+ * signal). Payload `{durationMs, error}`. Distinct from `RUN_FAILED`
601
+ * (which implies a step's `process()` threw cleanly). Currently
602
+ * manual via `tracker.markRunCrashed(runId, {error})`.
603
+ */
604
+ | "RUN_CRASHED"
605
+ /**
606
+ * Tier 2 quick-wins — a step's final retry attempt exceeded its
607
+ * `maxDuration` cap. Payload `{durationMs, stepId, maxDurationMs,
608
+ * attemptsExhausted}`. Auto-emitted by `RunnerSteps` when the
609
+ * timeout fires on the last attempt. Run status flips to
610
+ * `"timedOut"`.
611
+ */
612
+ | "RUN_TIMED_OUT";
122
613
  export interface RunEvent {
123
614
  id: string;
124
615
  type: RunEventType;
@@ -155,6 +646,26 @@ export interface WorkflowDetail extends WorkflowSummary {
155
646
  definition?: unknown;
156
647
  nodeNames: string[];
157
648
  runtimes: string[];
649
+ /**
650
+ * Sample-body for the Studio empty-state curl. `source` lets the
651
+ * UI optionally distinguish author-declared examples from the
652
+ * synthesized ones (it doesn't today, but could in the future).
653
+ * Always populated for object-shaped workflows; `body` is `{}`
654
+ * when no body references were detected.
655
+ */
656
+ examples?: {
657
+ body: unknown;
658
+ /**
659
+ * Provenance of the body. `author` = declared in the workflow's
660
+ * `trigger.http.examples.body`. `recorded` = captured from the
661
+ * first successful run when `recordSample: true` is set on the
662
+ * trigger. `inferred` = synthesized from static analysis of step
663
+ * inputs. `empty` = no body references + no recording + no
664
+ * author override (the literal `{}`). Studio surfaces this in
665
+ * the Runs-tab empty-state curl snippet label.
666
+ */
667
+ source: "author" | "recorded" | "inferred" | "empty";
668
+ };
158
669
  }
159
670
  export interface PaginatedResult<T> {
160
671
  data: T[];
@@ -170,6 +681,43 @@ export interface StartRunOptions {
170
681
  nodeCount: number;
171
682
  tags?: string[];
172
683
  metadata?: Record<string, unknown>;
684
+ /**
685
+ * Tier 1 · replay lineage. When a run is started via the replay endpoint,
686
+ * this carries the original run's id so Studio can render a "Replay of #..."
687
+ * breadcrumb. Plumbed end-to-end via the `X-Blok-Replay-Of` HTTP header.
688
+ */
689
+ replayOf?: string;
690
+ /**
691
+ * Tier 2 · sub-workflow lineage. Populated by `SubworkflowNode` when
692
+ * invoking a child workflow inline. Studio uses this to render a
693
+ * "called from #..." breadcrumb on the child's run detail.
694
+ */
695
+ parentRunId?: string;
696
+ parentNodeRunId?: string;
697
+ /**
698
+ * Tier 2 #5 · scheduled dispatch time (ms since epoch). When set, the
699
+ * run is created with status `"delayed"` instead of `"running"`.
700
+ */
701
+ scheduledAt?: number;
702
+ /**
703
+ * Tier 2 #5 · TTL deadline (ms since epoch). Persists onto the run
704
+ * for the dispatcher to consult.
705
+ */
706
+ expiresAt?: number;
707
+ /**
708
+ * Tier 2 #7 · resolved debounce key. When set, pings sharing this
709
+ * key + workflow name coalesce.
710
+ */
711
+ debounceKey?: string;
712
+ /**
713
+ * Tier 2 #7 · debounce mode that produced this run.
714
+ */
715
+ debounceMode?: "leading" | "trailing";
716
+ /**
717
+ * Tier 2 #7 · initial ping count for the run (typically 1 — the
718
+ * first ping). Subsequent pings increment via direct store update.
719
+ */
720
+ pingCount?: number;
173
721
  }
174
722
  export interface StartNodeOptions {
175
723
  nodeName: string;
@@ -179,6 +727,42 @@ export interface StartNodeOptions {
179
727
  parentNodeId?: string;
180
728
  depth: number;
181
729
  stepIndex: number;
730
+ /**
731
+ * Tier 2 #4 sub-workflow mode. Only meaningful when `nodeType === "subworkflow"`.
732
+ * Persisted onto `NodeRun.wait` so Studio can render `↳ async` vs `↳ sub`.
733
+ */
734
+ wait?: boolean;
735
+ /**
736
+ * G2 (v0.6) sub-workflow dispatch strategy. Only meaningful when
737
+ * `nodeType === "subworkflow"`. Persisted onto `NodeRun.dispatch` so
738
+ * Studio can render an extra `http` badge alongside `↳ async`/`↳ sub`
739
+ * for HTTP self-call dispatches.
740
+ */
741
+ dispatch?: "in-process" | "http-self";
742
+ /**
743
+ * PR 5 E3 — sub-workflow nesting depth. Only meaningful when
744
+ * `nodeType === "subworkflow"`. The runner computes
745
+ * `parentDepth + 1` from `ctx._subworkflowDepth` at start-time and
746
+ * passes here so Studio can render `↳ sub (N)` for N >= 2.
747
+ */
748
+ subworkflowDepth?: number;
749
+ /**
750
+ * v0.5 — origin middleware name. Set when the parent ctx carries
751
+ * `_blokMiddlewareName` (HttpTrigger.runMiddlewareChain stash).
752
+ * Propagates onto `NodeRun.middleware` so Studio's StepRail can
753
+ * surface a `mw:<name>` badge.
754
+ */
755
+ middleware?: string;
756
+ /**
757
+ * v0.5.3 — iteration index for steps inside a `forEach` or `loop`
758
+ * primitive. Set when the parent ctx carries `_blokIterationIndex`
759
+ * (ForEachNode + LoopNode stash on each per-iteration child ctx).
760
+ * Studio's StepRail uses this to group consecutive sibling rows
761
+ * into "iteration N" sections instead of showing them flat with
762
+ * duplicated step names. Undefined for top-level steps and for
763
+ * inner steps of non-iteration primitives (`tryCatch`, `switch`).
764
+ */
765
+ iterationIndex?: number;
182
766
  }
183
767
  export type WidgetType = "stat-card" | "timeline" | "error-rate" | "duration-distribution" | "workflow-breakdown" | "node-performance" | "recent-runs" | "heatmap";
184
768
  export interface DashboardWidget {
@@ -208,10 +792,96 @@ export interface Dashboard {
208
792
  updatedAt: number;
209
793
  widgets: DashboardWidget[];
210
794
  }
795
+ /**
796
+ * E2 — server-side saved filter for the runs list. Mirrors the
797
+ * earlier localStorage shape (`apps/studio/src/lib/savedFilters.ts`)
798
+ * with `id` + timestamps added. Filter names are UNIQUE across the
799
+ * deployment — re-saving overwrites the existing row. Studio applies
800
+ * them by setting the run-list filter UI from the stored values.
801
+ */
802
+ export interface SavedFilter {
803
+ id: string;
804
+ name: string;
805
+ /** Workflow run status (`""` for "all"). */
806
+ status: string;
807
+ /**
808
+ * Free-form tag input as the user typed it ("env:prod,team:billing").
809
+ * Server stores it verbatim; the run-list URL parser interprets it.
810
+ */
811
+ tagsInput: string;
812
+ /**
813
+ * Free-form metadata input as the user typed it
814
+ * ("tier=premium,region__in=us,eu"). Same parsing contract as the
815
+ * runs-page URL — `MetadataFilter[]` once parsed.
816
+ */
817
+ metadataInput: string;
818
+ createdAt: number;
819
+ updatedAt: number;
820
+ }
821
+ /**
822
+ * Sample-body recording (v0.6 follow-up to #100). When a workflow's
823
+ * HTTP trigger declares `recordSample: true`, the trigger captures
824
+ * the request body of the FIRST successful run and persists it
825
+ * here. Studio's `examples.body` resolution prefers a recorded
826
+ * sample over static inference but always defers to an author-
827
+ * declared `trigger.http.examples.body`.
828
+ *
829
+ * One row per workflow — re-recording is intentionally skipped
830
+ * after the first capture so storage stays bounded + the
831
+ * operator-visible example stays stable. Authors can re-trigger a
832
+ * recording by deleting the row (no Studio surface for this yet;
833
+ * call `tracker.deleteWorkflowSample(name)` from a script).
834
+ */
835
+ export interface WorkflowSample {
836
+ workflowName: string;
837
+ body: unknown;
838
+ /** Run id whose request body was captured. */
839
+ sourceRunId: string;
840
+ /** Wall-clock when the sample was recorded. */
841
+ recordedAt: number;
842
+ }
843
+ /**
844
+ * F2 (v0.5) — operators accepted on metadata filters. `eq` is the
845
+ * default (and the only one available before v0.5). Each operator
846
+ * coerces values the way an operator typically would: numerical
847
+ * comparisons cast to number, `like` is SQL `LIKE` (with `%` /
848
+ * `_` wildcards), `in`/`nin` accept array values.
849
+ */
850
+ export type MetadataOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "like" | "in" | "nin";
851
+ /**
852
+ * F2 (v0.5) — operator-aware metadata filter. Combines with other
853
+ * filters in the same `RunQuery.metadata` array via AND semantics.
854
+ *
855
+ * `value` is `string` for scalar operators (`eq` / `ne` / `gt` / `lt` /
856
+ * `gte` / `lte` / `like`), `string[]` for set operators (`in` / `nin`).
857
+ * Numeric comparisons treat the stored metadata value as a number when
858
+ * possible (the underlying JSON store preserves the native type).
859
+ */
860
+ export interface MetadataFilter {
861
+ key: string;
862
+ op: MetadataOp;
863
+ value: string | string[];
864
+ }
211
865
  export interface RunQuery {
212
866
  workflow?: string;
213
867
  status?: WorkflowRunStatus;
214
868
  tags?: string[];
869
+ /**
870
+ * Filter by metadata key/value pairs. Multiple entries combine with
871
+ * AND semantics (a run matches only when every declared filter is
872
+ * satisfied).
873
+ *
874
+ * **Two accepted shapes** — `Record<string, string>` is back-compat
875
+ * sugar for an array of `{key, op: "eq", value}` filters. Pass an
876
+ * array to use operators beyond `=` (F2: `ne` / `gt` / `gte` / `lt`
877
+ * / `lte` / `like` / `in` / `nin`). Both shapes route through the
878
+ * same evaluator after normalisation.
879
+ *
880
+ * **Performance** — sequential JSON-extract scan by default;
881
+ * declare hot keys via `BLOK_INDEXED_METADATA_KEYS=tier,region` to
882
+ * get an indexed column + index on the SqliteRunStore side (F1).
883
+ */
884
+ metadata?: Record<string, string> | MetadataFilter[];
215
885
  limit?: number;
216
886
  offset?: number;
217
887
  sort?: "asc" | "desc";