@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.
|
|
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.
|
|
51
|
-
"@wooksjs/event-http": "^0.7.
|
|
52
|
-
"
|
|
53
|
-
"
|
|
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.
|
|
58
|
-
"@wooksjs/event-http": "^0.7.
|
|
59
|
-
"
|
|
60
|
-
"
|
|
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
|
}
|
package/scripts/setup-skills.js
DELETED
|
@@ -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.
|