@blokjs/helper 0.2.1 → 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.
- package/dist/components/AddElse.d.ts +15 -0
- package/dist/components/AddIf.d.ts +15 -0
- package/dist/components/StepNode.d.ts +6 -0
- package/dist/components/StepNode.js +8 -0
- package/dist/components/StepNode.js.map +1 -1
- package/dist/components/Trigger.d.ts +10 -2
- package/dist/components/Trigger.js +16 -5
- package/dist/components/Trigger.js.map +1 -1
- package/dist/components/branch.d.ts +44 -0
- package/dist/components/branch.js +57 -0
- package/dist/components/branch.js.map +1 -0
- package/dist/components/forEach.d.ts +53 -0
- package/dist/components/forEach.js +64 -0
- package/dist/components/forEach.js.map +1 -0
- package/dist/components/loop.d.ts +52 -0
- package/dist/components/loop.js +54 -0
- package/dist/components/loop.js.map +1 -0
- package/dist/components/switchOn.d.ts +68 -0
- package/dist/components/switchOn.js +76 -0
- package/dist/components/switchOn.js.map +1 -0
- package/dist/components/tryCatch.d.ts +63 -0
- package/dist/components/tryCatch.js +68 -0
- package/dist/components/tryCatch.js.map +1 -0
- package/dist/components/workflowV2.d.ts +83 -0
- package/dist/components/workflowV2.js +84 -0
- package/dist/components/workflowV2.js.map +1 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +25 -4
- package/dist/index.js.map +1 -1
- package/dist/proxy/$.d.ts +102 -0
- package/dist/proxy/$.js +130 -0
- package/dist/proxy/$.js.map +1 -0
- package/dist/types/StepOpts.d.ts +723 -3
- package/dist/types/StepOpts.js +702 -3
- package/dist/types/StepOpts.js.map +1 -1
- package/dist/types/TriggerOpts.d.ts +1600 -35
- package/dist/types/TriggerOpts.js +601 -29
- package/dist/types/TriggerOpts.js.map +1 -1
- package/dist/types/WorkflowOpts.d.ts +478 -28
- package/dist/types/WorkflowOpts.js +66 -3
- package/dist/types/WorkflowOpts.js.map +1 -1
- package/dist/utils/parseDuration.d.ts +33 -0
- package/dist/utils/parseDuration.js +78 -0
- package/dist/utils/parseDuration.js.map +1 -0
- package/dist/workflow.schema.json +662 -0
- package/package.json +6 -6
package/dist/types/StepOpts.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { DurationSchema } from "./TriggerOpts";
|
|
2
3
|
/**
|
|
3
|
-
* RuntimeKind represents all supported runtime environments
|
|
4
|
-
*
|
|
4
|
+
* RuntimeKind represents all supported runtime environments.
|
|
5
|
+
*
|
|
6
|
+
* Synced with `@blokjs/runner` `RuntimeKind` type.
|
|
5
7
|
*/
|
|
6
8
|
export const RuntimeKindSchema = z.enum([
|
|
7
9
|
"nodejs",
|
|
@@ -17,7 +19,7 @@ export const RuntimeKindSchema = z.enum([
|
|
|
17
19
|
"wasm",
|
|
18
20
|
]);
|
|
19
21
|
/**
|
|
20
|
-
* Node type enum
|
|
22
|
+
* Node type enum — includes both legacy types and new runtime types.
|
|
21
23
|
*/
|
|
22
24
|
export const NodeTypeSchema = z.enum([
|
|
23
25
|
"local",
|
|
@@ -36,6 +38,19 @@ export const NodeTypeSchema = z.enum([
|
|
|
36
38
|
"runtime.docker",
|
|
37
39
|
"runtime.wasm",
|
|
38
40
|
]);
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// V1 — Legacy step shape. Kept for backward compatibility.
|
|
43
|
+
// New workflows should use `StepV2Schema` via the `workflow()` factory.
|
|
44
|
+
// =============================================================================
|
|
45
|
+
/**
|
|
46
|
+
* Validation schema for a single workflow step (v1 — legacy).
|
|
47
|
+
*
|
|
48
|
+
* Mirrors the JSON workflow step shape so the TypeScript DSL produces
|
|
49
|
+
* structurally-identical output to JSON workflows.
|
|
50
|
+
*
|
|
51
|
+
* @deprecated Prefer {@link StepV2Schema}. v1 shapes are still accepted and
|
|
52
|
+
* normalized at workflow load time.
|
|
53
|
+
*/
|
|
39
54
|
export const StepOptsSchema = z.object({
|
|
40
55
|
name: z
|
|
41
56
|
.string({
|
|
@@ -52,6 +67,9 @@ export const StepOptsSchema = z.object({
|
|
|
52
67
|
type: NodeTypeSchema,
|
|
53
68
|
inputs: z.object({}).optional(),
|
|
54
69
|
runtime: RuntimeKindSchema.optional(),
|
|
70
|
+
active: z.boolean().optional(),
|
|
71
|
+
stop: z.boolean().optional(),
|
|
72
|
+
stream_logs: z.boolean().optional(),
|
|
55
73
|
});
|
|
56
74
|
// It is used globally in the project
|
|
57
75
|
export const StepInputsSchema = z.object({}, { message: "Inputs required" });
|
|
@@ -59,4 +77,685 @@ export const StepConditionSchema = z.object({
|
|
|
59
77
|
node: StepOptsSchema,
|
|
60
78
|
conditions: z.function().optional(),
|
|
61
79
|
});
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// V2 — Canonical step shape. LLM- and human-friendly.
|
|
82
|
+
// =============================================================================
|
|
83
|
+
/**
|
|
84
|
+
* Retry configuration for a v2 step.
|
|
85
|
+
*
|
|
86
|
+
* Wraps `step.process(ctx, step)` in a retry loop with capped exponential
|
|
87
|
+
* backoff. Per-attempt failures emit `NODE_ATTEMPT_FAILED` trace events.
|
|
88
|
+
*
|
|
89
|
+
* Defaults applied by the runner when fields are omitted:
|
|
90
|
+
* - `minTimeoutInMs`: 1000
|
|
91
|
+
* - `maxTimeoutInMs`: 30000
|
|
92
|
+
* - `factor`: 2
|
|
93
|
+
*
|
|
94
|
+
* Shape mirrors Trigger.dev's `retry` config so authors moving between
|
|
95
|
+
* platforms read familiar semantics. No jitter is added — matches
|
|
96
|
+
* Trigger.dev's default.
|
|
97
|
+
*/
|
|
98
|
+
export const RetryConfigSchema = z
|
|
99
|
+
.object({
|
|
100
|
+
maxAttempts: z
|
|
101
|
+
.number()
|
|
102
|
+
.int()
|
|
103
|
+
.min(1)
|
|
104
|
+
.max(20)
|
|
105
|
+
.describe("Total attempts including the first run. 1 = no retry. Capped at 20."),
|
|
106
|
+
minTimeoutInMs: z
|
|
107
|
+
.number()
|
|
108
|
+
.int()
|
|
109
|
+
.min(0)
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Initial backoff delay in ms before the second attempt. Default 1000."),
|
|
112
|
+
maxTimeoutInMs: z
|
|
113
|
+
.number()
|
|
114
|
+
.int()
|
|
115
|
+
.min(0)
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("Cap on the backoff delay between attempts. Default 30000."),
|
|
118
|
+
factor: z
|
|
119
|
+
.number()
|
|
120
|
+
.min(1)
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Exponential backoff factor: delay = min(maxTimeout, minTimeout * factor^(attempt-1)). Default 2."),
|
|
123
|
+
})
|
|
124
|
+
.refine((r) => r.minTimeoutInMs === undefined || r.maxTimeoutInMs === undefined || r.minTimeoutInMs <= r.maxTimeoutInMs, {
|
|
125
|
+
message: "`minTimeoutInMs` must be <= `maxTimeoutInMs`.",
|
|
126
|
+
path: ["maxTimeoutInMs"],
|
|
127
|
+
});
|
|
128
|
+
/**
|
|
129
|
+
* V2 regular step — invokes a node with inputs.
|
|
130
|
+
*
|
|
131
|
+
* **Identity**
|
|
132
|
+
* - `id` is the step's stable identifier. Other steps reference this step's
|
|
133
|
+
* output as `$.state[id]`.
|
|
134
|
+
* - `use` is the node reference (e.g. `@blokjs/api-call`).
|
|
135
|
+
*
|
|
136
|
+
* **Persistence (default-store rule)**
|
|
137
|
+
* Every step's `result.data` is automatically stored in `ctx.state[id]`
|
|
138
|
+
* after execution. This is the 95% case. The four declarative knobs:
|
|
139
|
+
* - `as: "<name>"` — store at `state[name]` instead of `state[id]`.
|
|
140
|
+
* - `spread: true` — shallow-merge keys of `result.data` into `state`
|
|
141
|
+
* (data-pipeline pattern). Mutually exclusive with `as`.
|
|
142
|
+
* - `ephemeral: true` — skip storage; only `ctx.prev` carries the result
|
|
143
|
+
* to the next step.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* { id: "fetch-users", use: "postgres-query", inputs: { sql: "..." } }
|
|
147
|
+
* // state["fetch-users"] = result.data
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* { id: "step-1", use: "...", as: "users" }
|
|
151
|
+
* // state.users = result.data
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* { id: "load", use: "fetch-user-and-profile", spread: true }
|
|
155
|
+
* // result.data = { user, profile } -> state.user + state.profile
|
|
156
|
+
*/
|
|
157
|
+
export const V2RegularStepSchema = z
|
|
158
|
+
.object({
|
|
159
|
+
id: z
|
|
160
|
+
.string({
|
|
161
|
+
required_error: "Step id is required",
|
|
162
|
+
invalid_type_error: "Step id must be a string",
|
|
163
|
+
})
|
|
164
|
+
.min(1)
|
|
165
|
+
.describe("Stable identifier. Other steps reference this step's output as $.state[id]. Required."),
|
|
166
|
+
use: z
|
|
167
|
+
.string({
|
|
168
|
+
required_error: "Step `use` is required",
|
|
169
|
+
invalid_type_error: "Step `use` must be a string",
|
|
170
|
+
})
|
|
171
|
+
.min(1)
|
|
172
|
+
.describe("Node reference. Examples: '@blokjs/api-call', 'my-custom-node'. " +
|
|
173
|
+
"Type is inferred from this value when `type` is not set."),
|
|
174
|
+
type: NodeTypeSchema.optional().describe("Node type (module/local/runtime.*). When omitted, inferred from `use`: " +
|
|
175
|
+
"runtime.* prefixes are explicit; @blokjs/* and most others default to 'module'."),
|
|
176
|
+
inputs: z
|
|
177
|
+
.record(z.unknown())
|
|
178
|
+
.optional()
|
|
179
|
+
.describe("Inputs passed to the node. May contain $ proxy references " +
|
|
180
|
+
"(e.g. $.state.foo, $.req.body.id) or 'js/...' expressions for runtime evaluation."),
|
|
181
|
+
as: z
|
|
182
|
+
.string()
|
|
183
|
+
.min(1)
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("Alternative name for this step's output in state. " +
|
|
186
|
+
"Defaults to `id`. Useful when the id is implementation-detail-y " +
|
|
187
|
+
"and the output is referenced by a domain term."),
|
|
188
|
+
spread: z
|
|
189
|
+
.boolean()
|
|
190
|
+
.optional()
|
|
191
|
+
.describe("If true, the result.data object's top-level keys are shallow-merged into state. " +
|
|
192
|
+
"Use for multi-output nodes in data-pipeline workflows. " +
|
|
193
|
+
"Mutually exclusive with `as`."),
|
|
194
|
+
ephemeral: z
|
|
195
|
+
.boolean()
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("If true, this step's output is NOT stored in state. " +
|
|
198
|
+
"Only ctx.prev carries it to the immediately next step. " +
|
|
199
|
+
"Use for side-effects (logging, audit, telemetry)."),
|
|
200
|
+
runtime: RuntimeKindSchema.optional().describe("Optional runtime hint. Most authors don't need this; the type already encodes it."),
|
|
201
|
+
active: z.boolean().optional().describe("If false, the step is skipped at runtime. Default true."),
|
|
202
|
+
stop: z.boolean().optional().describe("If true, the workflow halts after this step completes. Default false."),
|
|
203
|
+
stream_logs: z
|
|
204
|
+
.boolean()
|
|
205
|
+
.optional()
|
|
206
|
+
.describe("Per-step opt-in for live log streaming. Inherits from BLOK_STREAM_LOGS env when unset."),
|
|
207
|
+
idempotencyKey: z
|
|
208
|
+
.string()
|
|
209
|
+
.min(1)
|
|
210
|
+
.optional()
|
|
211
|
+
.describe("When set, the step's result is cached against the triple " +
|
|
212
|
+
"(workflowName, step.id, idempotencyKey). On a subsequent run with the " +
|
|
213
|
+
"same triple, execution is skipped and the cached result populates state " +
|
|
214
|
+
"through the same persistence rules (ephemeral / spread / as). " +
|
|
215
|
+
"Accepts a literal string or a $ proxy expression that compiles to " +
|
|
216
|
+
"`js/ctx....` (e.g. $.req.body.requestId)."),
|
|
217
|
+
idempotencyKeyTTL: z
|
|
218
|
+
.number()
|
|
219
|
+
.int()
|
|
220
|
+
.min(0)
|
|
221
|
+
.optional()
|
|
222
|
+
.describe("Cache lifetime in milliseconds. Defaults to 24h (86_400_000) when omitted. " +
|
|
223
|
+
"Pass 0 to mark a cached result as immediately expired (effectively disables caching)."),
|
|
224
|
+
retry: RetryConfigSchema.optional().describe("Retry configuration with capped exponential backoff. " +
|
|
225
|
+
"When omitted, the step runs at most once (no retry) — matches pre-v0.3.x behavior."),
|
|
226
|
+
maxDuration: DurationSchema.optional().describe("OPTIONAL. Per-attempt execution timeout. Number (ms) or duration " +
|
|
227
|
+
"string ('30s', '5m', '500ms'). When the step's `step.process()` " +
|
|
228
|
+
"exceeds this duration, the attempt fails with a StepTimeoutError. " +
|
|
229
|
+
"Pairs with `retry` — each attempt gets its own timeout (total " +
|
|
230
|
+
"budget = maxDuration × maxAttempts). On final-attempt timeout, the " +
|
|
231
|
+
'run auto-flips to `"timedOut"` status (distinct from `"failed"` ' +
|
|
232
|
+
"so SLA dashboards can separate timeouts from logic failures)."),
|
|
233
|
+
})
|
|
234
|
+
.refine((step) => !(step.as && step.spread), {
|
|
235
|
+
message: "`as` and `spread` are mutually exclusive — pick one.",
|
|
236
|
+
path: ["spread"],
|
|
237
|
+
});
|
|
238
|
+
/**
|
|
239
|
+
* V2 branch step — `branch({when, then, else})`.
|
|
240
|
+
*
|
|
241
|
+
* Replaces the legacy `addCondition + new AddIf().addStep().build()` pattern.
|
|
242
|
+
* Compiles down to the existing `@blokjs/if-else` flow node at workflow
|
|
243
|
+
* load time, so the runner core needs no change.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* {
|
|
247
|
+
* id: "route-by-method",
|
|
248
|
+
* branch: {
|
|
249
|
+
* when: '$.req.method === "POST"',
|
|
250
|
+
* then: [{ id: "create", use: "...", inputs: {...} }],
|
|
251
|
+
* else: [{ id: "read", use: "...", inputs: {...} }]
|
|
252
|
+
* }
|
|
253
|
+
* }
|
|
254
|
+
*/
|
|
255
|
+
export const V2BranchStepSchema = z.lazy(() => z.object({
|
|
256
|
+
id: z.string().min(1).describe("Stable identifier for the branch step. Visible in traces."),
|
|
257
|
+
branch: z
|
|
258
|
+
.object({
|
|
259
|
+
when: z
|
|
260
|
+
.string()
|
|
261
|
+
.min(1)
|
|
262
|
+
.describe("JavaScript expression. Truthy → run `then` branch; falsy → run `else` branch. " +
|
|
263
|
+
"$ proxy expressions compile to strings at the call site (e.g. $.req.query.kind === 'true')."),
|
|
264
|
+
then: z.array(z.unknown()).describe("Steps to execute when `when` is truthy."),
|
|
265
|
+
else: z.array(z.unknown()).optional().describe("Steps to execute when `when` is falsy. Optional."),
|
|
266
|
+
})
|
|
267
|
+
.describe("Conditional sub-pipeline."),
|
|
268
|
+
active: z.boolean().optional(),
|
|
269
|
+
stop: z.boolean().optional(),
|
|
270
|
+
}));
|
|
271
|
+
/**
|
|
272
|
+
* V2 sub-workflow step — invoke another named workflow inline.
|
|
273
|
+
*
|
|
274
|
+
* The parent step blocks until the child workflow completes (`wait: true`,
|
|
275
|
+
* the default). The child gets its own `ctx`, its own trace run record,
|
|
276
|
+
* and runs through the same `RunnerSteps` machinery as a top-level run.
|
|
277
|
+
* The child's `ctx.response` becomes the parent step's output, so it
|
|
278
|
+
* lands on `state[<id>]` like any other step (mirrors HTTP semantics:
|
|
279
|
+
* sub-workflow looks like a function call).
|
|
280
|
+
*
|
|
281
|
+
* Inputs flow from parent → child as `ctx.request.body` — the child
|
|
282
|
+
* reads them via `$.req.body.<key>` exactly as if it had been
|
|
283
|
+
* HTTP-triggered.
|
|
284
|
+
*
|
|
285
|
+
* **Composition with Tier 1**:
|
|
286
|
+
* - `idempotencyKey` on this step caches the entire sub-workflow's
|
|
287
|
+
* result. Cache hit = child workflow is NEVER invoked (no side
|
|
288
|
+
* effects fire on rerun). Documented footgun + headline pattern.
|
|
289
|
+
* - `retry` retries the whole sub-workflow on failure.
|
|
290
|
+
* - Replay re-creates fresh sub-run lineage automatically.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* {
|
|
294
|
+
* id: "send-receipt",
|
|
295
|
+
* subworkflow: "send-receipt-email",
|
|
296
|
+
* inputs: { user: $.state.user, order: $.state.order },
|
|
297
|
+
* wait: true, // default; `wait: false` deferred to a follow-up
|
|
298
|
+
* idempotencyKey: $.req.body.requestId,
|
|
299
|
+
* }
|
|
300
|
+
*/
|
|
301
|
+
export const V2SubworkflowStepSchema = z.lazy(() => z
|
|
302
|
+
.object({
|
|
303
|
+
id: z
|
|
304
|
+
.string()
|
|
305
|
+
.min(1)
|
|
306
|
+
.describe("Stable identifier. The sub-workflow's output lands on $.state[id] " + "after the child completes. Required."),
|
|
307
|
+
subworkflow: z
|
|
308
|
+
.string()
|
|
309
|
+
.min(1)
|
|
310
|
+
.describe("Name of the workflow to invoke. Looked up in the WorkflowRegistry " +
|
|
311
|
+
'at run time. **Literal names** (`"send-receipt-email"`) are matched ' +
|
|
312
|
+
'directly. **Polymorphic expressions** (`"$.req.body.kind"`, ' +
|
|
313
|
+
'`"js/ctx.req.body.kind"`) resolve against the live ctx at dispatch ' +
|
|
314
|
+
"time — pair with `allowList` to constrain which workflows the " +
|
|
315
|
+
"expression may resolve to."),
|
|
316
|
+
inputs: z
|
|
317
|
+
.record(z.unknown())
|
|
318
|
+
.optional()
|
|
319
|
+
.describe("Inputs passed to the child as `ctx.request.body`. The child reads " +
|
|
320
|
+
"them via `$.req.body.<key>` exactly as if HTTP-triggered. " +
|
|
321
|
+
"May contain $ proxy refs."),
|
|
322
|
+
wait: z
|
|
323
|
+
.boolean()
|
|
324
|
+
.optional()
|
|
325
|
+
.describe("If true (default), parent step blocks until child completes and " +
|
|
326
|
+
"the child's ctx.response becomes the parent step's output. " +
|
|
327
|
+
"If false, dispatch is fire-and-forget — the parent step returns " +
|
|
328
|
+
"immediately with `{runId, workflowName, scheduledAt}` and the " +
|
|
329
|
+
"child runs asynchronously via setImmediate. The child still " +
|
|
330
|
+
"appears in Studio's Sub-runs strip and the parentRunId/parentNodeRunId " +
|
|
331
|
+
"lineage is preserved. Combine with `idempotencyKey` for " +
|
|
332
|
+
"at-most-once dispatch (Trigger.dev / Stripe semantics: the runId " +
|
|
333
|
+
"is cached against the key regardless of child outcome; new key " +
|
|
334
|
+
"needed to retry on failure)."),
|
|
335
|
+
as: z
|
|
336
|
+
.string()
|
|
337
|
+
.min(1)
|
|
338
|
+
.optional()
|
|
339
|
+
.describe("Alternative state key (defaults to id). Mutually exclusive with spread."),
|
|
340
|
+
spread: z
|
|
341
|
+
.boolean()
|
|
342
|
+
.optional()
|
|
343
|
+
.describe("Shallow-merge child's response keys into state. Mutually exclusive with as."),
|
|
344
|
+
ephemeral: z
|
|
345
|
+
.boolean()
|
|
346
|
+
.optional()
|
|
347
|
+
.describe("If true, child output is NOT stored in state. Only ctx.prev carries it."),
|
|
348
|
+
active: z.boolean().optional().describe("If false, the step is skipped at runtime. Default true."),
|
|
349
|
+
stop: z.boolean().optional().describe("If true, the workflow halts after this step completes."),
|
|
350
|
+
idempotencyKey: z
|
|
351
|
+
.string()
|
|
352
|
+
.min(1)
|
|
353
|
+
.optional()
|
|
354
|
+
.describe("When set, the sub-workflow's parent step output is cached against " +
|
|
355
|
+
"the triple (parentWorkflow, step.id, key). Cache semantics depend " +
|
|
356
|
+
"on `wait`: with `wait: true` (default), cache HIT means the child " +
|
|
357
|
+
"workflow is NEVER invoked — including any side effects (use with " +
|
|
358
|
+
"care for sub-workflows that send emails, charge cards, etc.). With " +
|
|
359
|
+
"`wait: false`, cache HIT returns the SAME `{runId, workflowName, " +
|
|
360
|
+
"scheduledAt}` for the lifetime of the cache entry — at-most-once " +
|
|
361
|
+
"dispatch deduplication. To retry on child failure, use a new key."),
|
|
362
|
+
idempotencyKeyTTL: z
|
|
363
|
+
.number()
|
|
364
|
+
.int()
|
|
365
|
+
.min(0)
|
|
366
|
+
.optional()
|
|
367
|
+
.describe("Cache lifetime in milliseconds. Defaults to 24h. Pass 0 to immediately expire."),
|
|
368
|
+
retry: RetryConfigSchema.optional().describe("Retry the WHOLE sub-workflow on failure. Each retry creates a fresh " +
|
|
369
|
+
"child run record under the same parent."),
|
|
370
|
+
maxDuration: DurationSchema.optional().describe("OPTIONAL. Per-attempt execution timeout. Caps the synchronous wait for " +
|
|
371
|
+
"`wait: true` sub-workflows. No-op for `wait: false` (parent returns " +
|
|
372
|
+
"immediately; the child's max-duration is the child's concern). Number " +
|
|
373
|
+
"(ms) or duration string. On final-attempt timeout, the run auto-flips " +
|
|
374
|
+
'to `"timedOut"`.'),
|
|
375
|
+
allowList: z
|
|
376
|
+
.array(z.string().min(1))
|
|
377
|
+
.optional()
|
|
378
|
+
.describe("Exact-match allow-list for polymorphic dispatch. When the resolved " +
|
|
379
|
+
"workflow name (after any `namespace` prefix is applied) is not in this " +
|
|
380
|
+
"array, the dispatch is rejected at run time with a structured error. " +
|
|
381
|
+
"Strongly recommended when `subworkflow` is an expression (`$.<path>` " +
|
|
382
|
+
"or `js/...`) so a malicious or buggy ctx value can't dispatch arbitrary " +
|
|
383
|
+
"workflows. Ignored for literal names (they don't need the guard)."),
|
|
384
|
+
dispatch: z
|
|
385
|
+
.enum(["in-process", "http-self"])
|
|
386
|
+
.optional()
|
|
387
|
+
.describe("G2 (v0.6) — dispatch strategy. `in-process` (default) invokes the " +
|
|
388
|
+
"child workflow in the same Node process — synchronous when `wait: true`, " +
|
|
389
|
+
"`setImmediate`-based when `wait: false`. `http-self` makes an HTTP " +
|
|
390
|
+
"request to the deployment's own base URL (`BLOK_SELF_BASE_URL` env " +
|
|
391
|
+
"var, defaults to `http://localhost:${PORT || 4000}`). Use `http-self` " +
|
|
392
|
+
"when you want each child run to land on a different process in a " +
|
|
393
|
+
"horizontally-scaled deployment, or to fully isolate child execution " +
|
|
394
|
+
"from the parent's call stack. The child must have an HTTP trigger; " +
|
|
395
|
+
"a runtime error is thrown otherwise. Lineage (parentRunId / " +
|
|
396
|
+
"parentNodeRunId) is preserved across the HTTP hop via " +
|
|
397
|
+
"`X-Blok-Parent-Run-Id` / `X-Blok-Parent-Node-Run-Id` headers."),
|
|
398
|
+
})
|
|
399
|
+
.refine((step) => !(step.as && step.spread), {
|
|
400
|
+
message: "`as` and `spread` are mutually exclusive — pick one.",
|
|
401
|
+
path: ["spread"],
|
|
402
|
+
}));
|
|
403
|
+
/**
|
|
404
|
+
* V2 wait step (PR 4 · `wait.for(duration)` / `wait.until(date)`).
|
|
405
|
+
*
|
|
406
|
+
* Pauses workflow execution mid-run for the specified duration (or until
|
|
407
|
+
* the absolute deadline). Composes with the durable scheduler — long
|
|
408
|
+
* waits survive process restart via the existing
|
|
409
|
+
* `scheduled_dispatches` infrastructure (PR 4 P3 adds
|
|
410
|
+
* `last_completed_step_index` so the runner skips past completed
|
|
411
|
+
* pre-wait steps on resume).
|
|
412
|
+
*
|
|
413
|
+
* Author surface:
|
|
414
|
+
* ```ts
|
|
415
|
+
* { id: "wait-3d", wait: { for: "3d" } }
|
|
416
|
+
* { id: "wait-deadline", wait: { until: $.req.body.scheduledAt } }
|
|
417
|
+
* ```
|
|
418
|
+
*
|
|
419
|
+
* Cannot combine with `idempotencyKey` (the wait IS the checkpoint) or
|
|
420
|
+
* `retry` (waits don't fail in a retryable way).
|
|
421
|
+
*/
|
|
422
|
+
export const V2WaitStepSchema = z
|
|
423
|
+
.object({
|
|
424
|
+
id: z.string().min(1).describe("Stable identifier."),
|
|
425
|
+
wait: z
|
|
426
|
+
.object({
|
|
427
|
+
for: DurationSchema.optional().describe("Wait this long. Mutually exclusive with `until`. " +
|
|
428
|
+
"Number (ms) or duration string (`500ms`, `30s`, `5m`, `2h`, `1d`)."),
|
|
429
|
+
until: z
|
|
430
|
+
.union([z.number(), z.string()])
|
|
431
|
+
.optional()
|
|
432
|
+
.describe("Wait until this absolute time. Number is ms-since-epoch; " +
|
|
433
|
+
"string is an ISO date or a $-proxy expression. Mutually exclusive with `for`."),
|
|
434
|
+
})
|
|
435
|
+
.strict(),
|
|
436
|
+
as: z.string().min(1).optional().describe("Alternative state key (defaults to `id`)."),
|
|
437
|
+
ephemeral: z.boolean().optional().describe("If true, no state entry is recorded."),
|
|
438
|
+
active: z.boolean().optional(),
|
|
439
|
+
stop: z.boolean().optional(),
|
|
440
|
+
// PR 1-5 polish + review fix-up — explicit rejection of fields that
|
|
441
|
+
// are meaningless on wait steps. `.strict()` below would reject
|
|
442
|
+
// these as "unrecognized key" with a generic message; `.never()`
|
|
443
|
+
// lets us produce a feature-specific error explaining WHY the
|
|
444
|
+
// field is rejected so authors don't have to guess. `.optional()`
|
|
445
|
+
// permits undefined (the normal case for wait steps that don't
|
|
446
|
+
// pass any of these fields).
|
|
447
|
+
idempotencyKey: z
|
|
448
|
+
.never({
|
|
449
|
+
errorMap: () => ({
|
|
450
|
+
message: "`idempotencyKey` is not supported on wait steps — the wait itself is the checkpoint.",
|
|
451
|
+
}),
|
|
452
|
+
})
|
|
453
|
+
.optional(),
|
|
454
|
+
retry: z
|
|
455
|
+
.never({
|
|
456
|
+
errorMap: () => ({
|
|
457
|
+
message: "`retry` is not supported on wait steps — waits don't fail in a retryable way.",
|
|
458
|
+
}),
|
|
459
|
+
})
|
|
460
|
+
.optional(),
|
|
461
|
+
// Review fix-up — three more rejections the original polish PR
|
|
462
|
+
// missed. All three could appear plausible to an author coming
|
|
463
|
+
// from regular steps; the helpful message saves them a
|
|
464
|
+
// debugging session.
|
|
465
|
+
maxDuration: z
|
|
466
|
+
.never({
|
|
467
|
+
errorMap: () => ({
|
|
468
|
+
message: "`maxDuration` is not supported on wait steps — the wait IS the duration.",
|
|
469
|
+
}),
|
|
470
|
+
})
|
|
471
|
+
.optional(),
|
|
472
|
+
concurrencyKey: z
|
|
473
|
+
.never({
|
|
474
|
+
errorMap: () => ({
|
|
475
|
+
message: "`concurrencyKey` is not supported on wait steps — concurrency gating lives on the trigger config, not on per-step waits.",
|
|
476
|
+
}),
|
|
477
|
+
})
|
|
478
|
+
.optional(),
|
|
479
|
+
spread: z
|
|
480
|
+
.never({
|
|
481
|
+
errorMap: () => ({
|
|
482
|
+
message: "`spread` is not supported on wait steps — wait steps produce no data to spread.",
|
|
483
|
+
}),
|
|
484
|
+
})
|
|
485
|
+
.optional(),
|
|
486
|
+
})
|
|
487
|
+
.strict()
|
|
488
|
+
.refine((s) => (s.wait.for !== undefined) !== (s.wait.until !== undefined), {
|
|
489
|
+
message: "`wait.for` and `wait.until` are mutually exclusive — pick one.",
|
|
490
|
+
path: ["wait"],
|
|
491
|
+
});
|
|
492
|
+
/**
|
|
493
|
+
* V2 forEach step — iterate over a collection running a sub-pipeline
|
|
494
|
+
* per item. Sequential (default) or parallel with bounded concurrency.
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* forEach({
|
|
498
|
+
* id: "process-orders",
|
|
499
|
+
* in: $.state.orders,
|
|
500
|
+
* as: "order",
|
|
501
|
+
* mode: "parallel",
|
|
502
|
+
* concurrency: 5,
|
|
503
|
+
* do: [
|
|
504
|
+
* { id: "charge", use: "stripe-charge", inputs: { amount: $.state.order.total } },
|
|
505
|
+
* ],
|
|
506
|
+
* })
|
|
507
|
+
*/
|
|
508
|
+
export const V2ForEachStepSchema = z.lazy(() => z.object({
|
|
509
|
+
id: z.string().min(1).describe("Stable identifier for the forEach step. Visible in traces."),
|
|
510
|
+
forEach: z
|
|
511
|
+
.object({
|
|
512
|
+
in: z
|
|
513
|
+
.unknown()
|
|
514
|
+
.describe("Array source. Literal expression string (`'$.state.items'`) or `$` proxy expression."),
|
|
515
|
+
as: z
|
|
516
|
+
.string()
|
|
517
|
+
.min(1)
|
|
518
|
+
.regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, "as must be a valid identifier (letters, digits, underscore)")
|
|
519
|
+
.describe("Per-iteration variable name. Each iteration sets ctx.state[as] = item and ctx.state[as+'Index'] = i."),
|
|
520
|
+
mode: z
|
|
521
|
+
.enum(["sequential", "parallel"])
|
|
522
|
+
.optional()
|
|
523
|
+
.describe("Execution mode. `sequential` (default) awaits each iteration; `parallel` runs with bounded concurrency."),
|
|
524
|
+
concurrency: z
|
|
525
|
+
.number()
|
|
526
|
+
.int()
|
|
527
|
+
.min(1)
|
|
528
|
+
.max(1000)
|
|
529
|
+
.optional()
|
|
530
|
+
.describe("Max concurrent inner pipelines when `mode: 'parallel'`. Default 10."),
|
|
531
|
+
do: z.array(z.unknown()).min(1).describe("Sub-pipeline run for each item."),
|
|
532
|
+
})
|
|
533
|
+
.describe("forEach configuration."),
|
|
534
|
+
active: z.boolean().optional(),
|
|
535
|
+
stop: z.boolean().optional(),
|
|
536
|
+
}));
|
|
537
|
+
/**
|
|
538
|
+
* V2 loop step — while-loop with hard maxIterations safety cap.
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* loop({
|
|
542
|
+
* id: "poll",
|
|
543
|
+
* while: '$.state["check-status"].status !== "done"',
|
|
544
|
+
* maxIterations: 60,
|
|
545
|
+
* do: [
|
|
546
|
+
* { id: "wait-tick", wait: { for: "2s" } },
|
|
547
|
+
* { id: "check-status", use: "@blokjs/api-call", inputs: { url: $.state.url } },
|
|
548
|
+
* ],
|
|
549
|
+
* })
|
|
550
|
+
*/
|
|
551
|
+
export const V2LoopStepSchema = z.lazy(() => z.object({
|
|
552
|
+
id: z.string().min(1).describe("Stable identifier for the loop step. Visible in traces."),
|
|
553
|
+
loop: z
|
|
554
|
+
.object({
|
|
555
|
+
while: z
|
|
556
|
+
.string()
|
|
557
|
+
.min(1)
|
|
558
|
+
.describe("JS expression evaluated against ctx before each iteration. Loop continues while truthy."),
|
|
559
|
+
maxIterations: z
|
|
560
|
+
.number()
|
|
561
|
+
.int()
|
|
562
|
+
.min(1)
|
|
563
|
+
.optional()
|
|
564
|
+
.describe("Hard safety cap on iterations. Default 1000 (override via env BLOK_LOOP_MAX_ITERATIONS). " +
|
|
565
|
+
"Hitting the cap throws LoopMaxIterationsError."),
|
|
566
|
+
do: z.array(z.unknown()).min(1).describe("Sub-pipeline run each iteration."),
|
|
567
|
+
})
|
|
568
|
+
.describe("loop configuration."),
|
|
569
|
+
active: z.boolean().optional(),
|
|
570
|
+
stop: z.boolean().optional(),
|
|
571
|
+
}));
|
|
572
|
+
/**
|
|
573
|
+
* V2 switch step — N-way branch keyed on a value. First matching case wins.
|
|
574
|
+
*
|
|
575
|
+
* `on` resolves to a value at run time (literal, `$` proxy expression, or
|
|
576
|
+
* `js/...` string). Each case carries a `when` and a `do` sub-pipeline:
|
|
577
|
+
* - `when` is a literal → match if `on === when`.
|
|
578
|
+
* - `when` is an array → match if `array.includes(on)` (group related cases).
|
|
579
|
+
* - `default` runs when no case matches. Optional.
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* switchOn({
|
|
583
|
+
* id: "route-by-tenant",
|
|
584
|
+
* on: $.req.headers["x-tenant-id"],
|
|
585
|
+
* cases: [
|
|
586
|
+
* { when: "acme", do: [{ id: "x", subworkflow: "acme-process" }] },
|
|
587
|
+
* { when: ["a","b"], do: [{ id: "y", subworkflow: "shared" }] },
|
|
588
|
+
* ],
|
|
589
|
+
* default: [{ id: "respond-403", use: "@blokjs/respond", stop: true,
|
|
590
|
+
* inputs: { status: 403, body: { error: "Unknown tenant" } } }],
|
|
591
|
+
* })
|
|
592
|
+
*/
|
|
593
|
+
export const V2SwitchStepSchema = z.lazy(() => z.object({
|
|
594
|
+
id: z.string().min(1).describe("Stable identifier for the switch step. Visible in traces."),
|
|
595
|
+
switch: z
|
|
596
|
+
.object({
|
|
597
|
+
on: z
|
|
598
|
+
.unknown()
|
|
599
|
+
.describe("Value to match against. Literal, `$` proxy expression, or `js/...` string. " +
|
|
600
|
+
"Resolved by the blueprint mapper before matching."),
|
|
601
|
+
cases: z
|
|
602
|
+
.array(z.object({
|
|
603
|
+
when: z
|
|
604
|
+
.unknown()
|
|
605
|
+
.describe("Match value. Literal scalar (number/string/boolean) for `on === when` " +
|
|
606
|
+
"matching, or an array for `array.includes(on)` matching."),
|
|
607
|
+
do: z.array(z.unknown()).min(1).describe("Sub-pipeline run when this case matches."),
|
|
608
|
+
}))
|
|
609
|
+
.min(1)
|
|
610
|
+
.describe("Ordered list of cases. First match wins."),
|
|
611
|
+
default: z.array(z.unknown()).optional().describe("Fallback sub-pipeline when no case matches. Optional."),
|
|
612
|
+
})
|
|
613
|
+
.describe("switch configuration."),
|
|
614
|
+
active: z.boolean().optional(),
|
|
615
|
+
stop: z.boolean().optional(),
|
|
616
|
+
}));
|
|
617
|
+
/**
|
|
618
|
+
* V2 tryCatch step — JS-like exception handling for sub-pipelines.
|
|
619
|
+
*
|
|
620
|
+
* - `try` block runs first.
|
|
621
|
+
* - On error, the `catch` block runs with `ctx.error` populated
|
|
622
|
+
* (`$.error.message`, `$.error.name`, `$.error.stack`). Errors thrown
|
|
623
|
+
* inside `catch` propagate to the next outer handler — they DO NOT
|
|
624
|
+
* re-trigger `catch`.
|
|
625
|
+
* - `finally` (if provided) runs unconditionally after try/catch — on
|
|
626
|
+
* normal completion, after a caught error, AND after an uncaught
|
|
627
|
+
* error from inside `catch`. Errors from `finally` propagate.
|
|
628
|
+
*
|
|
629
|
+
* State mutations from any block are visible to subsequent top-level
|
|
630
|
+
* steps (passthrough flow, like switch).
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* tryCatch({
|
|
634
|
+
* id: "saga",
|
|
635
|
+
* try: [
|
|
636
|
+
* { id: "create", use: "user-create", inputs: { email: $.req.body.email } },
|
|
637
|
+
* { id: "notify", use: "email-send", inputs: { to: $.state.create.email } },
|
|
638
|
+
* ],
|
|
639
|
+
* catch: [
|
|
640
|
+
* { id: "rollback", use: "user-delete",
|
|
641
|
+
* inputs: { userId: $.state.create.id, reason: $.error.message } },
|
|
642
|
+
* ],
|
|
643
|
+
* finally: [
|
|
644
|
+
* { id: "metric", use: "@blokjs/metrics-emit", inputs: { event: "saga-attempt" } },
|
|
645
|
+
* ],
|
|
646
|
+
* })
|
|
647
|
+
*/
|
|
648
|
+
export const V2TryCatchStepSchema = z.lazy(() => z.object({
|
|
649
|
+
id: z.string().min(1).describe("Stable identifier for the tryCatch step. Visible in traces."),
|
|
650
|
+
tryCatch: z
|
|
651
|
+
.object({
|
|
652
|
+
try: z
|
|
653
|
+
.array(z.unknown())
|
|
654
|
+
.min(1)
|
|
655
|
+
.describe("Sub-pipeline run first. If any step throws, control jumps to `catch`."),
|
|
656
|
+
catch: z
|
|
657
|
+
.array(z.unknown())
|
|
658
|
+
.min(1)
|
|
659
|
+
.describe("Sub-pipeline run when `try` throws. Has access to `$.error` " +
|
|
660
|
+
"(message, name, stack). Errors here propagate — they do NOT re-trigger catch."),
|
|
661
|
+
finally: z
|
|
662
|
+
.array(z.unknown())
|
|
663
|
+
.optional()
|
|
664
|
+
.describe("Sub-pipeline run unconditionally after try/catch. Runs even if " +
|
|
665
|
+
"`catch` itself throws. Errors here propagate."),
|
|
666
|
+
})
|
|
667
|
+
.describe("tryCatch configuration."),
|
|
668
|
+
active: z.boolean().optional(),
|
|
669
|
+
stop: z.boolean().optional(),
|
|
670
|
+
}));
|
|
671
|
+
/**
|
|
672
|
+
* Discriminated v2 step — regular, branch, sub-workflow, wait, forEach, loop, switch, or tryCatch.
|
|
673
|
+
*
|
|
674
|
+
* Discriminators (no `kind` field needed):
|
|
675
|
+
* - presence of `branch` → branch step
|
|
676
|
+
* - presence of `subworkflow` → sub-workflow step
|
|
677
|
+
* - presence of `wait` (object) → wait step
|
|
678
|
+
* - presence of `forEach` → forEach step (v0.5)
|
|
679
|
+
* - presence of `loop` → loop step (v0.5)
|
|
680
|
+
* - presence of `switch` → switch step (v0.5)
|
|
681
|
+
* - presence of `tryCatch` → tryCatch step (v0.5)
|
|
682
|
+
* - otherwise → regular step
|
|
683
|
+
*/
|
|
684
|
+
export const V2StepSchema = z.lazy(() => z.union([
|
|
685
|
+
V2BranchStepSchema,
|
|
686
|
+
V2SubworkflowStepSchema,
|
|
687
|
+
V2WaitStepSchema,
|
|
688
|
+
V2ForEachStepSchema,
|
|
689
|
+
V2LoopStepSchema,
|
|
690
|
+
V2SwitchStepSchema,
|
|
691
|
+
V2TryCatchStepSchema,
|
|
692
|
+
V2RegularStepSchema,
|
|
693
|
+
]));
|
|
694
|
+
/**
|
|
695
|
+
* Type guard — true when the step is a branch.
|
|
696
|
+
*/
|
|
697
|
+
export function isBranchStep(step) {
|
|
698
|
+
return typeof step === "object" && step !== null && "branch" in step;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Type guard — true when the step is a wait (PR 4 `wait.for` / `wait.until`).
|
|
702
|
+
*/
|
|
703
|
+
export function isWaitStep(step) {
|
|
704
|
+
return (typeof step === "object" &&
|
|
705
|
+
step !== null &&
|
|
706
|
+
"wait" in step &&
|
|
707
|
+
typeof step.wait === "object" &&
|
|
708
|
+
step.wait !== null &&
|
|
709
|
+
(step.wait?.for !== undefined ||
|
|
710
|
+
step.wait?.until !== undefined));
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Type guard — true when the step is a sub-workflow invocation.
|
|
714
|
+
*/
|
|
715
|
+
export function isSubworkflowStep(step) {
|
|
716
|
+
return (typeof step === "object" &&
|
|
717
|
+
step !== null &&
|
|
718
|
+
"subworkflow" in step &&
|
|
719
|
+
typeof step.subworkflow === "string");
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Type guard — true when the step is a forEach iteration (v0.5).
|
|
723
|
+
*/
|
|
724
|
+
export function isForEachStep(step) {
|
|
725
|
+
return (typeof step === "object" &&
|
|
726
|
+
step !== null &&
|
|
727
|
+
"forEach" in step &&
|
|
728
|
+
typeof step.forEach === "object" &&
|
|
729
|
+
step.forEach !== null);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Type guard — true when the step is a while-loop (v0.5).
|
|
733
|
+
*/
|
|
734
|
+
export function isLoopStep(step) {
|
|
735
|
+
return (typeof step === "object" &&
|
|
736
|
+
step !== null &&
|
|
737
|
+
"loop" in step &&
|
|
738
|
+
typeof step.loop === "object" &&
|
|
739
|
+
step.loop !== null);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Type guard — true when the step is an N-way switch (v0.5).
|
|
743
|
+
*/
|
|
744
|
+
export function isSwitchStep(step) {
|
|
745
|
+
return (typeof step === "object" &&
|
|
746
|
+
step !== null &&
|
|
747
|
+
"switch" in step &&
|
|
748
|
+
typeof step.switch === "object" &&
|
|
749
|
+
step.switch !== null);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Type guard — true when the step is a tryCatch (v0.5).
|
|
753
|
+
*/
|
|
754
|
+
export function isTryCatchStep(step) {
|
|
755
|
+
return (typeof step === "object" &&
|
|
756
|
+
step !== null &&
|
|
757
|
+
"tryCatch" in step &&
|
|
758
|
+
typeof step.tryCatch === "object" &&
|
|
759
|
+
step.tryCatch !== null);
|
|
760
|
+
}
|
|
62
761
|
//# sourceMappingURL=StepOpts.js.map
|