@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooksjs/event-wf",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "@wooksjs/event-wf",
5
5
  "keywords": [
6
6
  "app",
@@ -22,13 +22,8 @@
22
22
  "url": "git+https://github.com/wooksjs/wooksjs.git",
23
23
  "directory": "packages/event-wf"
24
24
  },
25
- "bin": {
26
- "wooksjs-event-wf-skill": "scripts/setup-skills.js"
27
- },
28
25
  "files": [
29
- "dist",
30
- "skills",
31
- "scripts"
26
+ "dist"
32
27
  ],
33
28
  "main": "dist/index.cjs",
34
29
  "module": "dist/index.mjs",
@@ -47,17 +42,17 @@
47
42
  "devDependencies": {
48
43
  "typescript": "^5.9.3",
49
44
  "vitest": "^3.2.4",
50
- "@wooksjs/event-core": "^0.7.8",
51
- "@wooksjs/event-http": "^0.7.8",
52
- "wooks": "^0.7.8",
53
- "@wooksjs/http-body": "^0.7.8"
45
+ "@wooksjs/event-core": "^0.7.9",
46
+ "@wooksjs/event-http": "^0.7.9",
47
+ "@wooksjs/http-body": "^0.7.9",
48
+ "wooks": "^0.7.9"
54
49
  },
55
50
  "peerDependencies": {
56
51
  "@prostojs/logger": "^0.4.3",
57
- "@wooksjs/event-core": "^0.7.8",
58
- "@wooksjs/event-http": "^0.7.8",
59
- "wooks": "^0.7.8",
60
- "@wooksjs/http-body": "^0.7.8"
52
+ "@wooksjs/event-core": "^0.7.9",
53
+ "@wooksjs/event-http": "^0.7.9",
54
+ "@wooksjs/http-body": "^0.7.9",
55
+ "wooks": "^0.7.9"
61
56
  },
62
57
  "peerDependenciesMeta": {
63
58
  "@wooksjs/event-http": {
@@ -68,7 +63,6 @@
68
63
  }
69
64
  },
70
65
  "scripts": {
71
- "build": "rolldown -c ../../rolldown.config.mjs",
72
- "setup-skills": "node scripts/setup-skills.js"
66
+ "build": "rolldown -c ../../rolldown.config.mjs"
73
67
  }
74
68
  }
@@ -1,77 +0,0 @@
1
- #!/usr/bin/env node
2
- /* prettier-ignore */
3
- 'use strict'
4
-
5
- const fs = require('fs')
6
- const path = require('path')
7
- const os = require('os')
8
-
9
- const SKILL_NAME = 'wooksjs-event-wf'
10
- const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
11
-
12
- if (!fs.existsSync(SKILL_SRC)) {
13
- console.error(`No skills found at ${SKILL_SRC}`)
14
- console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
15
- process.exit(1)
16
- }
17
-
18
- const AGENTS = {
19
- 'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
20
- 'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
21
- 'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
22
- 'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
23
- 'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
24
- }
25
-
26
- const args = process.argv.slice(2)
27
- const isGlobal = args.includes('--global') || args.includes('-g')
28
- const isPostinstall = args.includes('--postinstall')
29
- let installed = 0, skipped = 0
30
- const installedDirs = []
31
-
32
- for (const [agentName, cfg] of Object.entries(AGENTS)) {
33
- const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
34
- const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
35
-
36
- // In postinstall mode: silently skip agents that aren't set up globally
37
- if (isPostinstall || isGlobal) {
38
- if (!fs.existsSync(agentRootDir)) { skipped++; continue }
39
- }
40
-
41
- const dest = path.join(targetBase, SKILL_NAME)
42
- try {
43
- fs.mkdirSync(dest, { recursive: true })
44
- fs.cpSync(SKILL_SRC, dest, { recursive: true })
45
- console.log(`✅ ${agentName}: installed to ${dest}`)
46
- installed++
47
- if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
48
- } catch (err) {
49
- console.warn(`⚠️ ${agentName}: failed — ${err.message}`)
50
- }
51
- }
52
-
53
- // Add locally-installed skill dirs to .gitignore
54
- if (!isGlobal && installedDirs.length > 0) {
55
- const gitignorePath = path.join(process.cwd(), '.gitignore')
56
- let gitignoreContent = ''
57
- try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
58
- const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
59
- if (linesToAdd.length > 0) {
60
- const hasHeader = gitignoreContent.includes('# AI agent skills')
61
- const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
62
- + (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
63
- + linesToAdd.join('\n') + '\n'
64
- fs.appendFileSync(gitignorePath, block)
65
- console.log(`📝 Added ${linesToAdd.length} entries to .gitignore`)
66
- }
67
- }
68
-
69
- if (installed === 0 && isPostinstall) {
70
- // Silence is fine — no agents present, nothing to do
71
- } else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
72
- console.log('No agent directories detected. Try --global or run without it for project-local install.')
73
- } else if (installed === 0) {
74
- console.log('Nothing installed. Run without --global to install project-locally.')
75
- } else {
76
- console.log(`\n✨ Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
77
- }
@@ -1,42 +0,0 @@
1
- ---
2
- name: wooksjs-event-wf
3
- description: Wooks Workflow framework — composable, step-based workflow engine for Node.js. Load when building workflow/process automation with wooks; defining workflow steps and flows; using workflow composables (useWfState, useRouteParams, useLogger); working with @wooksjs/event-core context (key, cached, defineWook, defineEventKind); creating conditional and looping flows; resuming paused workflows; handling user input requirements; using string-based step handlers; attaching workflow spies; working with StepRetriableError; creating custom event context composables for workflows; sharing parent event context (eventContext) for HTTP integration; accessing HTTP composables from workflow steps.
4
- ---
5
-
6
- # @wooksjs/event-wf
7
-
8
- A composable workflow framework for Node.js built on async context (AsyncLocalStorage) and `@prostojs/wf`. Define steps and flows as composable units — steps execute sequentially with conditional branching, loops, pause/resume, and user input handling. Context is scoped per workflow execution.
9
-
10
- ## How to use this skill
11
-
12
- Read the domain file that matches the task. Do not load all files — only what you need.
13
-
14
- | Domain | File | Load when... |
15
- | ------------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
- | Event context (core machinery) | [event-core.md](event-core.md) | Understanding `EventContext`, `key()`/`cached()`/`defineWook()`/`defineEventKind()`/`slot()`, creating custom composables, lazy evaluation and caching, building your own `use*()` functions |
17
- | Workflow app setup | [core.md](core.md) | Creating a workflow app, `createWfApp`, starting/resuming workflows, error handling, spies, testing, logging, sharing parent event context (`eventContext`), HTTP integration |
18
- | Steps & flows | [workflows.md](workflows.md) | Defining steps (`app.step`), defining flows (`app.flow`), workflow schemas, conditions, loops, user input, parametric steps, `useWfState`, `StepRetriableError`, string-based handlers |
19
-
20
- ## Quick reference
21
-
22
- ```ts
23
- import { createWfApp } from '@wooksjs/event-wf'
24
-
25
- const app = createWfApp<{ result: number }>()
26
-
27
- app.step('add', {
28
- input: 'number',
29
- handler: 'ctx.result += input',
30
- })
31
-
32
- app.flow('calculate', [
33
- { id: 'add', input: 5 },
34
- { id: 'add', input: 2 },
35
- { condition: 'result < 10', steps: [{ id: 'add', input: 3 }] },
36
- ])
37
-
38
- const output = await app.start('calculate', { result: 0 })
39
- console.log(output.state.context) // { result: 10 }
40
- ```
41
-
42
- Key exports: `createWfApp()`, `useWfState()`, `useRouteParams()`, `useLogger()`, `StepRetriableError`.
@@ -1,466 +0,0 @@
1
- # Core Concepts — @wooksjs/event-wf
2
-
3
- > Covers workflow app creation, starting and resuming workflows, how the workflow adapter integrates with the event context system, error handling, spies, testing, and logging.
4
-
5
- For the underlying event context store API (`init`, `get`, `set`, `hook`, etc.) and how to create custom composables, see [event-core.md](event-core.md).
6
-
7
- ## Mental Model
8
-
9
- `@wooksjs/event-wf` is the workflow adapter for Wooks. It wraps the `@prostojs/wf` workflow engine, adding composable context management via `AsyncLocalStorage`. Each workflow execution gets its own isolated context store, and step handlers can call composable functions (`useWfState()`, `useRouteParams()`, etc.) from anywhere.
10
-
11
- Key principles:
12
-
13
- 1. **Steps are route handlers** — Steps are registered with IDs that are resolved via the Wooks router, supporting parametric step IDs (`:param`), wildcards, and regex constraints.
14
- 2. **Flows are schemas** — Flows define the execution order of steps, with conditions, loops, and branching.
15
- 3. **Pause and resume** — Workflows can pause for user input and resume from saved state.
16
- 4. **String-based handlers** — Step handlers can be JavaScript strings (e.g., `'ctx.result += input'`), making them storable in databases.
17
-
18
- ## Installation
19
-
20
- ```bash
21
- npm install wooks @wooksjs/event-wf
22
- ```
23
-
24
- ## Creating a Workflow App
25
-
26
- ```ts
27
- import { createWfApp } from '@wooksjs/event-wf'
28
-
29
- const app = createWfApp<{ result: number }>()
30
-
31
- app.step('increment', {
32
- handler: (ctx) => {
33
- ctx.result++
34
- },
35
- })
36
-
37
- app.flow('my-flow', [{ id: 'increment' }])
38
-
39
- const output = await app.start('my-flow', { result: 0 })
40
- console.log(output.state.context.result) // 1
41
- ```
42
-
43
- `createWfApp<T>(opts?, wooks?)` returns a `WooksWf<T>` instance. The generic `T` is the workflow context type.
44
-
45
- Options:
46
-
47
- ```ts
48
- interface TWooksWfOptions {
49
- onError?: (e: Error) => void // custom error handler
50
- onNotFound?: TWooksHandler // handler when flow not found
51
- onUnknownFlow?: (schemaId: string, raiseError: () => void) => unknown
52
- logger?: TConsoleBase // custom logger
53
- eventOptions?: EventContextOptions // event context options (logger, parent)
54
- router?: {
55
- ignoreTrailingSlash?: boolean
56
- ignoreCase?: boolean
57
- cacheLimit?: number
58
- }
59
- }
60
- ```
61
-
62
- ## Starting a Workflow
63
-
64
- ### `app.start(schemaId, inputContext, opts?)`
65
-
66
- Starts a new workflow execution from the beginning:
67
-
68
- ```ts
69
- const output = await app.start('my-flow', { result: 0 })
70
-
71
- // With options
72
- const output = await app.start(
73
- 'my-flow',
74
- { result: 0 },
75
- {
76
- input: 5,
77
- eventContext: current(),
78
- },
79
- )
80
- ```
81
-
82
- **Parameters:**
83
-
84
- - `schemaId` — The flow ID registered with `app.flow()`
85
- - `inputContext` — The initial context object (`T`)
86
- - `opts` — Optional `TWfRunOptions` object:
87
- - `input` — Input for the first step (consumed after execution)
88
- - `spy` — Spy function to observe step execution
89
- - `cleanup` — Cleanup function called when execution ends
90
- - `eventContext` — Parent `EventContext` to link to. Pass `current()` from within an active event scope (e.g. HTTP handler). The workflow creates a child context with `parent: current()`, so step handlers can access parent composables transparently via parent chain traversal.
91
-
92
- **Return value (`TFlowOutput<T, I, IR>`):**
93
-
94
- ```ts
95
- interface TFlowOutput<T, I, IR> {
96
- finished: boolean // true if workflow completed
97
- state: {
98
- schemaId: string // flow ID
99
- indexes: number[] // position in schema (for resume)
100
- context: T // final context state
101
- }
102
- inputRequired?: {
103
- // present if paused for input
104
- type: string // expected input type
105
- schemaId: string // step requiring input
106
- }
107
- stepResult?: IR // last step's return value
108
- resume?: (input?: I) => Promise<TFlowOutput<T, I, IR>> // resume function
109
- }
110
- ```
111
-
112
- ### Checking completion
113
-
114
- ```ts
115
- const output = await app.start('my-flow', { result: 0 })
116
-
117
- if (output.finished) {
118
- console.log('Final result:', output.state.context)
119
- } else if (output.inputRequired) {
120
- console.log('Workflow paused, needs:', output.inputRequired.type)
121
- // Save output.state for later resume
122
- }
123
- ```
124
-
125
- ## Resuming a Workflow
126
-
127
- ### `app.resume(state, opts?)`
128
-
129
- Resumes a previously paused workflow from saved state:
130
-
131
- ```ts
132
- // Resume with user-provided input
133
- const resumed = await app.resume(output.state, { input: userInput })
134
-
135
- // Simple retry (no input)
136
- const retried = await app.resume(output.state)
137
- ```
138
-
139
- The `opts` parameter accepts the same `TWfRunOptions` as `start()` — including `eventContext` to link to the active event context via a parent chain.
140
-
141
- ### Using the `resume()` function on output
142
-
143
- The output object includes a convenience `resume()` method:
144
-
145
- ```ts
146
- const output = await app.start('login-flow', {})
147
- if (!output.finished && output.resume) {
148
- const final = await output.resume(userCredentials)
149
- }
150
- ```
151
-
152
- ### Full pause/resume pattern
153
-
154
- ```ts
155
- const app = createWfApp<{ username?: string; authenticated?: boolean }>()
156
-
157
- app.step('get-credentials', {
158
- input: '{ username: string, password: string }',
159
- handler: (ctx, input) => {
160
- ctx.username = input.username
161
- ctx.authenticated = validate(input.username, input.password)
162
- },
163
- })
164
-
165
- app.step('welcome', {
166
- handler: (ctx) => console.log(`Welcome, ${ctx.username}!`),
167
- })
168
-
169
- app.flow('login', [{ id: 'get-credentials' }, { id: 'welcome' }])
170
-
171
- // Start — pauses at get-credentials because input is required
172
- const output = await app.start('login', {})
173
- // output.finished === false
174
- // output.inputRequired === { type: '{ username: string, password: string }', schemaId: 'get-credentials' }
175
-
176
- // Save state (e.g., to database)
177
- const savedState = JSON.stringify(output.state)
178
-
179
- // Later, resume with user input
180
- const state = JSON.parse(savedState)
181
- const final = await app.resume(state, { input: { username: 'alice', password: 'secret' } })
182
- // final.finished === true
183
- ```
184
-
185
- ## How Workflow Context Works
186
-
187
- When `start()` or `resume()` is called, the adapter creates a workflow-specific event context using `createWfContext` (or `resumeWfContext`). These are context factories that hardcode the `wfKind` and delegate to `createEventContext`:
188
-
189
- ```
190
- app.start(schemaId, inputContext, opts)
191
- → createWfContext(ctxOptions, seeds, async () => { ... })
192
- → createEventContext(ctxOptions, wfKind, seeds, fn)
193
- → AsyncLocalStorage.run(ctx, handler)
194
- → router matches flow ID → handler runs
195
- → workflow engine executes steps sequentially
196
- → each step can call useWfState(), useRouteParams(), etc.
197
- → composables call current() from @wooksjs/event-core
198
- → reads/writes the event context via key/cached accessors
199
- ```
200
-
201
- When `eventContext` is passed in opts, `ctxOptions` includes `parent: eventContext`, linking the WF context to the parent (e.g. HTTP) via the parent chain.
202
-
203
- ### The WF Event Kind
204
-
205
- The WF adapter defines its event kind with `defineEventKind`. Seeds are passed directly to the context factory:
206
-
207
- ```ts
208
- // Seeds for createWfContext / resumeWfContext
209
- interface WfSeeds {
210
- schemaId: string // flow ID being executed
211
- stepId: string | null // current step ID (set during step execution)
212
- inputContext: unknown // the workflow context object (T)
213
- indexes?: number[] // position for resume
214
- input?: unknown // input for current step
215
- }
216
- ```
217
-
218
- ### Custom Composables for Workflows
219
-
220
- Use `defineWook` and `key()` from `@wooksjs/event-core` to create custom composables that store data in the event context:
221
-
222
- ```ts
223
- import { defineWook, key } from '@wooksjs/event-core'
224
-
225
- const startTimeKey = key<number>('wf.metrics.startTime')
226
- const stepCountKey = key<number>('wf.metrics.stepCount')
227
-
228
- export const useWorkflowMetrics = defineWook((ctx) => {
229
- ctx.set(startTimeKey, Date.now())
230
- ctx.set(stepCountKey, 0)
231
-
232
- return {
233
- incrementSteps: () => ctx.set(stepCountKey, ctx.get(stepCountKey) + 1),
234
- getElapsed: () => Date.now() - ctx.get(startTimeKey),
235
- getStepCount: () => ctx.get(stepCountKey),
236
- }
237
- })
238
- ```
239
-
240
- For the full context store API and composable patterns, see the `@wooksjs/event-core` skill.
241
-
242
- ## Workflow Spies
243
-
244
- Spies observe step execution without modifying behavior. Attach globally or per-execution:
245
-
246
- ### Global spy (all workflows)
247
-
248
- ```ts
249
- const spy = (event, data) => {
250
- console.log(`[${event}]`, data)
251
- }
252
-
253
- app.attachSpy(spy)
254
-
255
- // Later, remove it:
256
- app.detachSpy(spy)
257
- ```
258
-
259
- ### Per-execution spy
260
-
261
- ```ts
262
- const output = await app.start(
263
- 'my-flow',
264
- { result: 0 },
265
- {
266
- spy: (event, ...args) => {
267
- if (event === 'step') {
268
- console.log('Step executed:', args)
269
- }
270
- },
271
- },
272
- )
273
- ```
274
-
275
- The spy function receives:
276
-
277
- - `event` — Event type (e.g., `'step'`)
278
- - Additional arguments vary by event type
279
-
280
- ## Error Handling
281
-
282
- ### Default behavior
283
-
284
- By default, errors call `console.error` and `process.exit(1)`.
285
-
286
- ### Custom error handler
287
-
288
- ```ts
289
- const app = createWfApp({
290
- onError: (error) => {
291
- console.error(`Workflow error: ${error.message}`)
292
- // Don't exit — handle gracefully
293
- },
294
- })
295
- ```
296
-
297
- ### Errors in workflows
298
-
299
- Errors thrown in step handlers propagate up from `app.start()` / `app.resume()`:
300
-
301
- ```ts
302
- try {
303
- const output = await app.start('my-flow', { result: 0 })
304
- } catch (error) {
305
- console.error('Workflow failed:', error.message)
306
- }
307
- ```
308
-
309
- ### `StepRetriableError`
310
-
311
- A special error type that signals the workflow can be retried with input:
312
-
313
- ```ts
314
- import { StepRetriableError } from '@wooksjs/event-wf'
315
-
316
- app.step('validate', {
317
- handler: (ctx) => {
318
- if (!ctx.token) {
319
- throw new StepRetriableError('Token required', {
320
- inputRequired: { type: 'string', schemaId: 'validate' },
321
- })
322
- }
323
- },
324
- })
325
- ```
326
-
327
- ## Sharing the Parent Event Context
328
-
329
- By default, `start()` and `resume()` create an isolated event context — step handlers cannot access composables from the calling scope (e.g., HTTP composables). When `eventContext` is passed, the workflow creates a **child context** with a parent link (`parent: current()`) instead of sharing the parent context directly. The child context seeds its own WF slots locally, and slot lookups that are not found in the child automatically traverse the parent chain. This means both WF composables and parent composables (e.g., HTTP) work transparently inside step handlers.
330
-
331
- ### Use case: accessing HTTP auth data in workflow steps
332
-
333
- ```ts
334
- import { current } from '@wooksjs/event-core'
335
- import { createHttpApp, useRequest } from '@wooksjs/event-http'
336
- import { createWfApp, useWfState } from '@wooksjs/event-wf'
337
-
338
- const wf = createWfApp<{ userId: string; role: string }>()
339
-
340
- wf.step('check-permissions', {
341
- handler: () => {
342
- const { ctx } = useWfState()
343
- // useRequest() works because the child context traverses the parent chain
344
- const { headers } = useRequest()
345
- const user = decodeToken(headers.authorization)
346
- ctx<{ userId: string; role: string }>().userId = user.id
347
- ctx<{ userId: string; role: string }>().role = user.role
348
- },
349
- })
350
-
351
- wf.flow('secure-action', ['check-permissions', 'do-work'])
352
-
353
- const http = createHttpApp()
354
-
355
- http.post('/actions/run', async () => {
356
- const output = await wf.start(
357
- 'secure-action',
358
- { userId: '', role: '' },
359
- { eventContext: current() },
360
- )
361
- return output.state.context
362
- })
363
- ```
364
-
365
- ### When to inherit vs isolate
366
-
367
- - **Inherit** (`eventContext: current()`) when the workflow runs entirely within a single HTTP request and steps need parent composables (auth, headers, cached user data). The child context links to the parent via a parent chain, keeping WF-specific slots isolated while providing transparent access to parent slots.
368
- - **Isolate** (default) when the workflow may pause and resume across different requests, or when it should be testable without a parent context.
369
-
370
- ## Sharing Router Between Adapters
371
-
372
- Multiple adapters can share the same Wooks router:
373
-
374
- ```ts
375
- import { Wooks } from 'wooks'
376
- import { createWfApp } from '@wooksjs/event-wf'
377
-
378
- const wooks = new Wooks()
379
- const app1 = createWfApp({}, wooks)
380
- const app2 = createWfApp({}, wooks) // shares the same routes
381
- ```
382
-
383
- Or share with another adapter (e.g., HTTP):
384
-
385
- ```ts
386
- import { createHttpApp } from '@wooksjs/event-http'
387
- import { createWfApp } from '@wooksjs/event-wf'
388
-
389
- const httpApp = createHttpApp()
390
- const wfApp = createWfApp({}, httpApp) // shares httpApp's router
391
- ```
392
-
393
- ## Testing
394
-
395
- Test workflows by calling `app.start()` directly with explicit contexts:
396
-
397
- ```ts
398
- import { createWfApp } from '@wooksjs/event-wf'
399
-
400
- const app = createWfApp<{ count: number }>()
401
-
402
- app.step('increment', {
403
- handler: (ctx) => {
404
- ctx.count++
405
- },
406
- })
407
-
408
- app.flow('test-flow', [{ id: 'increment' }, { id: 'increment' }])
409
-
410
- // Test:
411
- const output = await app.start('test-flow', { count: 0 })
412
- expect(output.state.context.count).toBe(2)
413
- expect(output.finished).toBe(true)
414
- ```
415
-
416
- ### Testing resume
417
-
418
- ```ts
419
- app.step('needs-input', {
420
- input: 'number',
421
- handler: 'ctx.count += input',
422
- })
423
-
424
- app.flow('resume-flow', [{ id: 'needs-input' }])
425
-
426
- const output = await app.start('resume-flow', { count: 0 })
427
- expect(output.finished).toBe(false)
428
-
429
- const final = await app.resume(output.state, { input: 42 })
430
- expect(final.state.context.count).toBe(42)
431
- expect(final.finished).toBe(true)
432
- ```
433
-
434
- ## Logging
435
-
436
- Inside a step handler, use the event-scoped logger:
437
-
438
- ```ts
439
- import { useLogger } from '@wooksjs/event-core'
440
-
441
- app.step('process', {
442
- handler: (ctx) => {
443
- const logger = useLogger()
444
- logger.info('Processing...')
445
- ctx.processed = true
446
- },
447
- })
448
- ```
449
-
450
- ## Best Practices
451
-
452
- - **Use `createWfApp<T>()` with a typed context** — The generic `T` gives type safety for all step handlers and flow output.
453
- - **Use string handlers for storable logic** — When workflows are defined in a database, use string handlers like `'ctx.result += input'`.
454
- - **Use function handlers for complex logic** — When handlers need imports, async operations, or composables, use function handlers.
455
- - **Save state for resume** — `output.state` is serializable. Store it in a database to resume later.
456
- - **Use spies for logging/monitoring** — Don't add logging inside every step; attach a spy instead.
457
- - **Use `flow` init functions** — The optional `init` callback in `app.flow()` runs before the first step, useful for context setup.
458
-
459
- ## Gotchas
460
-
461
- - **Composables must be called within a step handler** (inside the async context). Calling them at module load time throws.
462
- - **`start()` and `resume()` return promises** — Always `await` them.
463
- - **Input is cleared after the first step** — When starting with `input`, it's only available to the first step. Subsequent steps don't see it unless the workflow pauses and resumes with new input.
464
- - **String handlers run in a restricted environment** — They can't access `require`, `import`, `process`, or other Node.js globals. Use function handlers for those.
465
- - **Step resolution uses the router** — Step IDs are looked up via the Wooks router. If a step ID contains `/`, it's treated as path segments for routing.
466
- - **Flow IDs also use routing** — You can have parametric flow IDs like `'process/:type'` and use `useRouteParams()` inside the flow's init function.