@wooksjs/event-wf 0.7.8 → 0.7.9
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.
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
# Steps & Flows — @wooksjs/event-wf
|
|
2
|
-
|
|
3
|
-
> Covers defining steps, defining flows (schemas), workflow schema syntax (conditions, loops, subflows), parametric steps, accessing workflow state with `useWfState`, string-based handlers, user input handling, and `StepRetriableError`.
|
|
4
|
-
|
|
5
|
-
## Defining Steps
|
|
6
|
-
|
|
7
|
-
### `app.step(id, opts)`
|
|
8
|
-
|
|
9
|
-
Registers a reusable step with a unique ID:
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
import { createWfApp } from '@wooksjs/event-wf'
|
|
13
|
-
|
|
14
|
-
const app = createWfApp<{ result: number }>()
|
|
15
|
-
|
|
16
|
-
// Function handler
|
|
17
|
-
app.step('double', {
|
|
18
|
-
handler: (ctx) => {
|
|
19
|
-
ctx.result *= 2
|
|
20
|
-
},
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
// String handler (storable, runs in restricted env)
|
|
24
|
-
app.step('add', {
|
|
25
|
-
input: 'number',
|
|
26
|
-
handler: 'ctx.result += input',
|
|
27
|
-
})
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
**Parameters:**
|
|
31
|
-
|
|
32
|
-
- `id` — Step identifier (used in flow schemas to reference this step). Supports router syntax: `'add/:n'`, `'process/*'`, etc.
|
|
33
|
-
- `opts.handler` — Either a function `(ctx: T, input?: I) => void | IR` or a JavaScript string.
|
|
34
|
-
- `opts.input` — Optional: input type description (string). When present and no input is provided at runtime, the workflow pauses to request input.
|
|
35
|
-
|
|
36
|
-
### Function handlers
|
|
37
|
-
|
|
38
|
-
Function handlers receive the workflow context and optional input:
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
app.step('process-item', {
|
|
42
|
-
handler: (ctx, input) => {
|
|
43
|
-
// ctx = workflow context (type T)
|
|
44
|
-
// input = step input (type I, from flow schema or resume)
|
|
45
|
-
ctx.items.push(input)
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Function handlers can use composables:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
import { useRouteParams } from '@wooksjs/event-core'
|
|
54
|
-
import { useWfState } from '@wooksjs/event-wf'
|
|
55
|
-
|
|
56
|
-
app.step('add/:n', {
|
|
57
|
-
handler: () => {
|
|
58
|
-
const { ctx } = useWfState()
|
|
59
|
-
const context = ctx<{ result: number }>()
|
|
60
|
-
context.result += Number(useRouteParams().get('n'))
|
|
61
|
-
},
|
|
62
|
-
})
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### String handlers
|
|
66
|
-
|
|
67
|
-
String handlers are JavaScript expressions evaluated in a restricted sandbox. They have access to `ctx` (context) and `input`:
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
app.step('add', {
|
|
71
|
-
input: 'number',
|
|
72
|
-
handler: 'ctx.result += input',
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
app.step('set-name', {
|
|
76
|
-
input: 'string',
|
|
77
|
-
handler: 'ctx.name = input',
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
app.step('multiply', {
|
|
81
|
-
handler: 'ctx.result *= 2',
|
|
82
|
-
})
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
String handlers are useful when workflow definitions are stored in a database — they can be serialized and loaded dynamically.
|
|
86
|
-
|
|
87
|
-
**Restrictions:** String handlers cannot access `require`, `import`, `process`, `fs`, or other Node.js globals. They only see `ctx` and `input`.
|
|
88
|
-
|
|
89
|
-
## Defining Flows
|
|
90
|
-
|
|
91
|
-
### `app.flow(id, schema, prefix?, init?)`
|
|
92
|
-
|
|
93
|
-
Registers a flow (workflow schema) — an ordered sequence of steps:
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
app.flow('calculate', [{ id: 'add', input: 5 }, { id: 'add', input: 2 }, { id: 'double' }])
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
**Parameters:**
|
|
100
|
-
|
|
101
|
-
- `id` — Flow identifier. Supports router syntax (e.g., `'process/:type'`, `'batch/*'`).
|
|
102
|
-
- `schema` — Array of step references, conditions, and loops (see Schema Syntax below).
|
|
103
|
-
- `prefix` — Optional prefix prepended to step IDs during resolution.
|
|
104
|
-
- `init` — Optional async function called before the first step executes.
|
|
105
|
-
|
|
106
|
-
### Flow init function
|
|
107
|
-
|
|
108
|
-
The `init` callback runs in the workflow context before any step executes:
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
app.flow('my-flow', ['step1', 'step2'], '', () => {
|
|
112
|
-
const { ctx } = useWfState()
|
|
113
|
-
const context = ctx<{ result: number }>()
|
|
114
|
-
// Modify context before steps run
|
|
115
|
-
context.result = 0
|
|
116
|
-
})
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
## Schema Syntax
|
|
120
|
-
|
|
121
|
-
Flow schemas are arrays of step references and control structures.
|
|
122
|
-
|
|
123
|
-
### Step references
|
|
124
|
-
|
|
125
|
-
Three forms for referencing steps in a schema:
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
// 1. String shorthand (step ID, optionally with parametric segments)
|
|
129
|
-
app.flow('f1', ['step1', 'add/5', 'add/10'])
|
|
130
|
-
|
|
131
|
-
// 2. Object with ID and optional input
|
|
132
|
-
app.flow('f2', [
|
|
133
|
-
{ id: 'add', input: 5 },
|
|
134
|
-
{ id: 'process', input: { key: 'value' } },
|
|
135
|
-
])
|
|
136
|
-
|
|
137
|
-
// 3. Mixed
|
|
138
|
-
app.flow('f3', ['step1', { id: 'add', input: 5 }, 'step2'])
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Conditional execution
|
|
142
|
-
|
|
143
|
-
Skip steps or groups based on runtime conditions:
|
|
144
|
-
|
|
145
|
-
```ts
|
|
146
|
-
app.flow('order', [
|
|
147
|
-
'check-inventory',
|
|
148
|
-
// Single step with condition
|
|
149
|
-
{ id: 'apply-discount', condition: 'order.total > 100' },
|
|
150
|
-
// Group of steps with condition (subflow)
|
|
151
|
-
{
|
|
152
|
-
condition: 'order.type !== "digital"',
|
|
153
|
-
steps: ['pack-item', 'ship-item'],
|
|
154
|
-
},
|
|
155
|
-
'send-confirmation',
|
|
156
|
-
])
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Conditions are JavaScript expressions evaluated against the workflow context. They have access to all context properties directly (not via `ctx.` prefix — the context is the scope).
|
|
160
|
-
|
|
161
|
-
```ts
|
|
162
|
-
// If context is { result: 5, items: [] }:
|
|
163
|
-
// condition: 'result > 3' → true
|
|
164
|
-
// condition: 'items.length > 0' → false
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### Loops (`while`)
|
|
168
|
-
|
|
169
|
-
Repeat a group of steps while a condition is true:
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
app.flow('retry-flow', [
|
|
173
|
-
{
|
|
174
|
-
while: 'attempts < 5 && !success',
|
|
175
|
-
steps: [
|
|
176
|
-
{ id: 'attempt' },
|
|
177
|
-
{ break: 'success' }, // break when success is truthy
|
|
178
|
-
],
|
|
179
|
-
},
|
|
180
|
-
])
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Loop constructs:
|
|
184
|
-
|
|
185
|
-
- `while` — Condition string evaluated before each iteration
|
|
186
|
-
- `break` — Condition string; if truthy, exits the loop
|
|
187
|
-
- `continue` — Condition string; if truthy, skips to next iteration
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
app.flow('process-batch', [
|
|
191
|
-
{
|
|
192
|
-
while: 'index < items.length',
|
|
193
|
-
steps: [
|
|
194
|
-
{ id: 'skip-invalid', continue: '!items[index].valid' },
|
|
195
|
-
{ id: 'process-item' },
|
|
196
|
-
{ id: 'increment-index' },
|
|
197
|
-
],
|
|
198
|
-
},
|
|
199
|
-
])
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Nested subflows
|
|
203
|
-
|
|
204
|
-
Subflows group steps for conditional execution or organizational clarity:
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
app.flow('deploy', [
|
|
208
|
-
'build',
|
|
209
|
-
{
|
|
210
|
-
condition: 'env === "production"',
|
|
211
|
-
steps: [
|
|
212
|
-
'run-tests',
|
|
213
|
-
'run-security-scan',
|
|
214
|
-
{
|
|
215
|
-
condition: 'securityPassed',
|
|
216
|
-
steps: ['deploy-to-prod', 'notify-team'],
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
condition: 'env === "staging"',
|
|
222
|
-
steps: ['deploy-to-staging'],
|
|
223
|
-
},
|
|
224
|
-
])
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
## Parametric Steps
|
|
228
|
-
|
|
229
|
-
Step IDs support the same router syntax as HTTP routes:
|
|
230
|
-
|
|
231
|
-
### Named parameters
|
|
232
|
-
|
|
233
|
-
```ts
|
|
234
|
-
app.step('add/:n', {
|
|
235
|
-
handler: () => {
|
|
236
|
-
const { ctx } = useWfState()
|
|
237
|
-
const context = ctx<{ result: number }>()
|
|
238
|
-
context.result += Number(useRouteParams().get('n'))
|
|
239
|
-
},
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
// Use in flow with different values
|
|
243
|
-
app.flow('calculate', ['add/5', 'add/10', 'add/3'])
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Regex-constrained parameters
|
|
247
|
-
|
|
248
|
-
```ts
|
|
249
|
-
app.step('multiply/:factor(\\d+)', {
|
|
250
|
-
handler: () => {
|
|
251
|
-
const { ctx } = useWfState()
|
|
252
|
-
ctx<{ result: number }>().result *= Number(useRouteParams().get('factor'))
|
|
253
|
-
},
|
|
254
|
-
})
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### Wildcard steps
|
|
258
|
-
|
|
259
|
-
```ts
|
|
260
|
-
app.step('log/*', {
|
|
261
|
-
handler: () => {
|
|
262
|
-
const message = useRouteParams().get('*')
|
|
263
|
-
console.log(message)
|
|
264
|
-
},
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
app.flow('verbose', ['log/starting', 'process', 'log/done'])
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### Parametric flow IDs
|
|
271
|
-
|
|
272
|
-
Flows can also have parametric IDs:
|
|
273
|
-
|
|
274
|
-
```ts
|
|
275
|
-
app.flow('process/:type', ['validate', 'transform', 'save'])
|
|
276
|
-
|
|
277
|
-
// Start with different types
|
|
278
|
-
await app.start('process/csv', { data: rawData })
|
|
279
|
-
await app.start('process/json', { data: rawData })
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
## Accessing Workflow State
|
|
283
|
-
|
|
284
|
-
### `useWfState()`
|
|
285
|
-
|
|
286
|
-
The primary composable for accessing workflow execution state from within step handlers:
|
|
287
|
-
|
|
288
|
-
```ts
|
|
289
|
-
import { useWfState } from '@wooksjs/event-wf'
|
|
290
|
-
|
|
291
|
-
app.step('my-step', {
|
|
292
|
-
handler: () => {
|
|
293
|
-
const { ctx, input, schemaId, stepId, indexes, resume } = useWfState()
|
|
294
|
-
|
|
295
|
-
ctx<MyContext>() // the workflow context object (type T)
|
|
296
|
-
input<MyInput>() // the current step's input (or undefined)
|
|
297
|
-
schemaId // the flow ID being executed
|
|
298
|
-
stepId() // the current step ID
|
|
299
|
-
indexes() // position in schema (for resume tracking)
|
|
300
|
-
resume // boolean: true if this is a resumed execution
|
|
301
|
-
},
|
|
302
|
-
})
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### `ctx<T>()`
|
|
306
|
-
|
|
307
|
-
Returns the workflow context — the mutable state shared across all steps:
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
app.step('transform', {
|
|
311
|
-
handler: () => {
|
|
312
|
-
const { ctx } = useWfState()
|
|
313
|
-
const context = ctx<{ items: string[]; processed: boolean }>()
|
|
314
|
-
context.items = context.items.map((s) => s.toUpperCase())
|
|
315
|
-
context.processed = true
|
|
316
|
-
},
|
|
317
|
-
})
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### `input<I>()`
|
|
321
|
-
|
|
322
|
-
Returns the input provided for this step (from the flow schema or from resume):
|
|
323
|
-
|
|
324
|
-
```ts
|
|
325
|
-
app.step('configure', {
|
|
326
|
-
handler: () => {
|
|
327
|
-
const { input } = useWfState()
|
|
328
|
-
const config = input<{ port: number; host: string }>()
|
|
329
|
-
if (config) {
|
|
330
|
-
// Use the provided input
|
|
331
|
-
}
|
|
332
|
-
},
|
|
333
|
-
})
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
### Using `useRouteParams()` in steps
|
|
337
|
-
|
|
338
|
-
For parametric step IDs, use `useRouteParams()` from `@wooksjs/event-core`:
|
|
339
|
-
|
|
340
|
-
```ts
|
|
341
|
-
import { useRouteParams } from '@wooksjs/event-core'
|
|
342
|
-
|
|
343
|
-
app.step('set/:key/:value', {
|
|
344
|
-
handler: () => {
|
|
345
|
-
const { ctx } = useWfState()
|
|
346
|
-
const { get } = useRouteParams<{ key: string; value: string }>()
|
|
347
|
-
const context = ctx<Record<string, string>>()
|
|
348
|
-
context[get('key')] = get('value')
|
|
349
|
-
},
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
app.flow('setup', ['set/name/Alice', 'set/role/admin'])
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
## User Input & Pause/Resume
|
|
356
|
-
|
|
357
|
-
When a step declares an `input` type but no input is provided in the schema, the workflow pauses:
|
|
358
|
-
|
|
359
|
-
```ts
|
|
360
|
-
app.step('get-email', {
|
|
361
|
-
input: 'string', // declares expected input type
|
|
362
|
-
handler: 'ctx.email = input',
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
app.step('send-welcome', {
|
|
366
|
-
handler: (ctx) => sendEmail(ctx.email, 'Welcome!'),
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
app.flow('onboarding', [
|
|
370
|
-
{ id: 'get-email' }, // no input provided → workflow pauses
|
|
371
|
-
{ id: 'send-welcome' },
|
|
372
|
-
])
|
|
373
|
-
|
|
374
|
-
// Start the workflow
|
|
375
|
-
const output = await app.start('onboarding', {})
|
|
376
|
-
// output.finished === false
|
|
377
|
-
// output.inputRequired.type === 'string'
|
|
378
|
-
|
|
379
|
-
// Resume with user's email
|
|
380
|
-
const final = await app.resume(output.state, { input: 'user@example.com' })
|
|
381
|
-
// final.finished === true
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
### Providing input in schema (no pause)
|
|
385
|
-
|
|
386
|
-
When input is provided in the schema, the step executes immediately:
|
|
387
|
-
|
|
388
|
-
```ts
|
|
389
|
-
app.flow('auto-onboarding', [
|
|
390
|
-
{ id: 'get-email', input: 'default@example.com' }, // input provided → no pause
|
|
391
|
-
{ id: 'send-welcome' },
|
|
392
|
-
])
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
### State serialization
|
|
396
|
-
|
|
397
|
-
The `output.state` object is plain JSON — serialize it for persistence:
|
|
398
|
-
|
|
399
|
-
```ts
|
|
400
|
-
// Save
|
|
401
|
-
await db.save('workflow:123', JSON.stringify(output.state))
|
|
402
|
-
|
|
403
|
-
// Load and resume
|
|
404
|
-
const saved = JSON.parse(await db.load('workflow:123'))
|
|
405
|
-
const result = await app.resume(saved, { input: userInput })
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
## StepRetriableError
|
|
409
|
-
|
|
410
|
-
A special error type for recoverable failures. When thrown, the workflow can be resumed:
|
|
411
|
-
|
|
412
|
-
```ts
|
|
413
|
-
import { StepRetriableError } from '@wooksjs/event-wf'
|
|
414
|
-
|
|
415
|
-
app.step('fetch-data', {
|
|
416
|
-
handler: async (ctx) => {
|
|
417
|
-
try {
|
|
418
|
-
ctx.data = await fetchFromApi()
|
|
419
|
-
} catch (e) {
|
|
420
|
-
throw new StepRetriableError('API temporarily unavailable')
|
|
421
|
-
}
|
|
422
|
-
},
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
// Handle retry
|
|
426
|
-
try {
|
|
427
|
-
await app.start('my-flow', {})
|
|
428
|
-
} catch (error) {
|
|
429
|
-
if (error instanceof StepRetriableError) {
|
|
430
|
-
// Wait and retry
|
|
431
|
-
await sleep(5000)
|
|
432
|
-
await app.resume(error.state, { input: retryInput })
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
## Common Patterns
|
|
438
|
-
|
|
439
|
-
### Pattern: Calculator workflow
|
|
440
|
-
|
|
441
|
-
```ts
|
|
442
|
-
const app = createWfApp<{ result: number }>()
|
|
443
|
-
|
|
444
|
-
app.step('add', { input: 'number', handler: 'ctx.result += input' })
|
|
445
|
-
app.step('multiply', { input: 'number', handler: 'ctx.result *= input' })
|
|
446
|
-
app.step('add/:n', {
|
|
447
|
-
handler: () => {
|
|
448
|
-
const { ctx } = useWfState()
|
|
449
|
-
ctx<{ result: number }>().result += Number(useRouteParams().get('n'))
|
|
450
|
-
},
|
|
451
|
-
})
|
|
452
|
-
|
|
453
|
-
app.flow('calculate', [{ id: 'add', input: 10 }, { id: 'multiply', input: 2 }, 'add/5'])
|
|
454
|
-
|
|
455
|
-
const output = await app.start('calculate', { result: 0 })
|
|
456
|
-
// result: (0 + 10) * 2 + 5 = 25
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### Pattern: Conditional processing pipeline
|
|
460
|
-
|
|
461
|
-
```ts
|
|
462
|
-
const app = createWfApp<{
|
|
463
|
-
data: unknown
|
|
464
|
-
format: string
|
|
465
|
-
validated: boolean
|
|
466
|
-
output?: string
|
|
467
|
-
}>()
|
|
468
|
-
|
|
469
|
-
app.step('validate', {
|
|
470
|
-
handler: (ctx) => {
|
|
471
|
-
ctx.validated = isValid(ctx.data)
|
|
472
|
-
},
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
app.step('to-json', {
|
|
476
|
-
handler: (ctx) => {
|
|
477
|
-
ctx.output = JSON.stringify(ctx.data)
|
|
478
|
-
},
|
|
479
|
-
})
|
|
480
|
-
|
|
481
|
-
app.step('to-csv', {
|
|
482
|
-
handler: (ctx) => {
|
|
483
|
-
ctx.output = toCsv(ctx.data)
|
|
484
|
-
},
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
app.flow('export', [
|
|
488
|
-
{ id: 'validate' },
|
|
489
|
-
{ condition: '!validated', steps: [] }, // early exit if invalid
|
|
490
|
-
{ condition: 'format === "json"', steps: ['to-json'] },
|
|
491
|
-
{ condition: 'format === "csv"', steps: ['to-csv'] },
|
|
492
|
-
])
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### Pattern: Interactive wizard
|
|
496
|
-
|
|
497
|
-
```ts
|
|
498
|
-
const app = createWfApp<{
|
|
499
|
-
name?: string
|
|
500
|
-
email?: string
|
|
501
|
-
plan?: string
|
|
502
|
-
}>()
|
|
503
|
-
|
|
504
|
-
app.step('get-name', { input: 'string', handler: 'ctx.name = input' })
|
|
505
|
-
app.step('get-email', { input: 'string', handler: 'ctx.email = input' })
|
|
506
|
-
app.step('get-plan', { input: 'string', handler: 'ctx.plan = input' })
|
|
507
|
-
app.step('confirm', {
|
|
508
|
-
handler: (ctx) => {
|
|
509
|
-
console.log(`Name: ${ctx.name}, Email: ${ctx.email}, Plan: ${ctx.plan}`)
|
|
510
|
-
},
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
app.flow('signup', [{ id: 'get-name' }, { id: 'get-email' }, { id: 'get-plan' }, { id: 'confirm' }])
|
|
514
|
-
|
|
515
|
-
// Each step pauses for user input
|
|
516
|
-
let output = await app.start('signup', {})
|
|
517
|
-
output = await app.resume(output.state, { input: 'Alice' }) // name
|
|
518
|
-
output = await app.resume(output.state, { input: 'a@b.com' }) // email
|
|
519
|
-
output = await app.resume(output.state, { input: 'pro' }) // plan
|
|
520
|
-
// output.finished === true
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
### Pattern: Retry loop
|
|
524
|
-
|
|
525
|
-
```ts
|
|
526
|
-
const app = createWfApp<{ attempts: number; success: boolean; data?: unknown }>()
|
|
527
|
-
|
|
528
|
-
app.step('attempt', {
|
|
529
|
-
handler: async (ctx) => {
|
|
530
|
-
ctx.attempts++
|
|
531
|
-
try {
|
|
532
|
-
ctx.data = await unreliableApi()
|
|
533
|
-
ctx.success = true
|
|
534
|
-
} catch {
|
|
535
|
-
ctx.success = false
|
|
536
|
-
}
|
|
537
|
-
},
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
app.flow('resilient-fetch', [
|
|
541
|
-
{
|
|
542
|
-
while: 'attempts < 5 && !success',
|
|
543
|
-
steps: [{ id: 'attempt' }, { break: 'success' }],
|
|
544
|
-
},
|
|
545
|
-
])
|
|
546
|
-
|
|
547
|
-
const output = await app.start('resilient-fetch', {
|
|
548
|
-
attempts: 0,
|
|
549
|
-
success: false,
|
|
550
|
-
})
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
## Best Practices
|
|
554
|
-
|
|
555
|
-
- **Use typed contexts** — Always provide the generic `<T>` to `createWfApp<T>()` and `ctx<T>()` for type safety.
|
|
556
|
-
- **Use string handlers for simple mutations** — `'ctx.result += input'` is cleaner and more portable than a function for trivial operations.
|
|
557
|
-
- **Use function handlers for complex logic** — Anything needing composables, async operations, or imports should use function handlers.
|
|
558
|
-
- **Use parametric steps** — Instead of creating separate steps for `add/5`, `add/10`, etc., create one `add/:n` step and reference it in flows.
|
|
559
|
-
- **Use conditions for branching** — Don't implement branching in step handlers. Use schema-level conditions to keep flow logic declarative.
|
|
560
|
-
- **Serialize state for long-running workflows** — Use `output.state` to persist workflow progress to a database.
|
|
561
|
-
- **Use spies for observability** — Attach spies for logging, metrics, or debugging rather than adding instrumentation inside steps.
|
|
562
|
-
|
|
563
|
-
## Gotchas
|
|
564
|
-
|
|
565
|
-
- **Conditions access context properties directly** — The condition `'result > 10'` checks `context.result`, not a local variable. The entire context is the evaluation scope.
|
|
566
|
-
- **Input is only for the first step on start** — When calling `app.start(id, ctx, { input })`, the `input` is consumed by the first step. After that, `input` is cleared. Subsequent steps only get input if the workflow pauses and resumes.
|
|
567
|
-
- **String handlers are sandboxed** — No access to Node.js APIs, `require`, `import`, `console`, etc. Only `ctx` and `input` are available.
|
|
568
|
-
- **Step IDs are router paths** — A step ID `'process/items'` is treated as two path segments. Use `'process-items'` if you want a flat ID.
|
|
569
|
-
- **Flow `init` runs in context** — The init function has access to composables (`useWfState()`, `useRouteParams()`, etc.) because it runs inside the async event context.
|
|
570
|
-
- **`resume()` requires the exact state object** — The `state` from `output.state` contains `indexes` that track the workflow's position. Don't modify it.
|