@moostjs/event-wf 0.5.33 → 0.6.1
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/README.md +62 -0
- package/dist/index.cjs +96 -22
- package/dist/index.d.ts +82 -2
- package/dist/index.mjs +95 -21
- package/package.json +40 -34
- package/scripts/setup-skills.js +78 -0
- package/skills/moostjs-event-wf/SKILL.md +40 -0
- package/skills/moostjs-event-wf/core.md +138 -0
- package/skills/moostjs-event-wf/decorators.md +179 -0
- package/skills/moostjs-event-wf/execution.md +325 -0
- package/skills/moostjs-event-wf/integration.md +180 -0
- package/skills/moostjs-event-wf/schemas.md +203 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* prettier-ignore */
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
const SKILL_NAME = 'moostjs-event-wf'
|
|
11
|
+
const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(SKILL_SRC)) {
|
|
14
|
+
console.error(`No skills found at ${SKILL_SRC}`)
|
|
15
|
+
console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const AGENTS = {
|
|
20
|
+
'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
|
|
21
|
+
'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
|
|
22
|
+
'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
|
|
23
|
+
'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
|
|
24
|
+
'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const args = process.argv.slice(2)
|
|
28
|
+
const isGlobal = args.includes('--global') || args.includes('-g')
|
|
29
|
+
const isPostinstall = args.includes('--postinstall')
|
|
30
|
+
let installed = 0, skipped = 0
|
|
31
|
+
const installedDirs = []
|
|
32
|
+
|
|
33
|
+
for (const [agentName, cfg] of Object.entries(AGENTS)) {
|
|
34
|
+
const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
|
|
35
|
+
const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
|
|
36
|
+
|
|
37
|
+
// In postinstall mode: silently skip agents that aren't set up globally
|
|
38
|
+
if (isPostinstall || isGlobal) {
|
|
39
|
+
if (!fs.existsSync(agentRootDir)) { skipped++; continue }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dest = path.join(targetBase, SKILL_NAME)
|
|
43
|
+
try {
|
|
44
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
45
|
+
fs.cpSync(SKILL_SRC, dest, { recursive: true })
|
|
46
|
+
console.log(`✅ ${agentName}: installed to ${dest}`)
|
|
47
|
+
installed++
|
|
48
|
+
if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn(`⚠️ ${agentName}: failed — ${err.message}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Add locally-installed skill dirs to .gitignore
|
|
55
|
+
if (!isGlobal && installedDirs.length > 0) {
|
|
56
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore')
|
|
57
|
+
let gitignoreContent = ''
|
|
58
|
+
try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
|
|
59
|
+
const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
|
|
60
|
+
if (linesToAdd.length > 0) {
|
|
61
|
+
const hasHeader = gitignoreContent.includes('# AI agent skills')
|
|
62
|
+
const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
|
|
63
|
+
+ (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
|
|
64
|
+
+ linesToAdd.join('\n') + '\n'
|
|
65
|
+
fs.appendFileSync(gitignorePath, block)
|
|
66
|
+
console.log(`📝 Added ${linesToAdd.length} entries to .gitignore`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (installed === 0 && isPostinstall) {
|
|
71
|
+
// Silence is fine — no agents present, nothing to do
|
|
72
|
+
} else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
|
|
73
|
+
console.log('No agent directories detected. Try --global or run without it for project-local install.')
|
|
74
|
+
} else if (installed === 0) {
|
|
75
|
+
console.log('Nothing installed. Run without --global to install project-locally.')
|
|
76
|
+
} else {
|
|
77
|
+
console.log(`\n✨ Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
|
|
78
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: moostjs-event-wf
|
|
3
|
+
description: Use this skill when working with @moostjs/event-wf — to define workflow steps with @Step(), create workflow entry points with @Workflow() and @WorkflowSchema(), inject workflow data with @WorkflowParam(), set up MoostWf adapter, start and resume workflows with wf.start()/wf.resume(), handle pause/resume with inputRequired, use StepRetriableError for recoverable failures, observe execution with attachSpy(), access workflow state with useWfState(), build conditional and looping schemas, integrate workflows with HTTP/CLI handlers, or use wfKind for event-type-aware logic.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @moostjs/event-wf
|
|
7
|
+
|
|
8
|
+
Workflow event adapter for Moost, wrapping `@wooksjs/event-wf` and `@prostojs/wf`. Define workflow steps and flows using decorators, with full Moost DI, interceptors, and pipes.
|
|
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
|
+
| Core concepts & setup | [core.md](core.md) | Starting a new project, installing the package, understanding the mental model, configuring MoostWf |
|
|
17
|
+
| Decorators & composables | [decorators.md](decorators.md) | Using @Step, @Workflow, @WorkflowSchema, @WorkflowParam, useWfState(), wfKind |
|
|
18
|
+
| Schemas & flow control | [schemas.md](schemas.md) | Defining step sequences, conditions, loops, break/continue, nested sub-workflows |
|
|
19
|
+
| Execution & lifecycle | [execution.md](execution.md) | Starting/resuming workflows, reading TFlowOutput, pause/resume, StepRetriableError, spies |
|
|
20
|
+
| Integration | [integration.md](integration.md) | Triggering workflows from HTTP/CLI, using interceptors/pipes/DI in steps, multi-adapter setup |
|
|
21
|
+
|
|
22
|
+
## Quick reference
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import {
|
|
26
|
+
MoostWf, Step, Workflow, WorkflowSchema, WorkflowParam,
|
|
27
|
+
StepRetriableError, useWfState, wfKind,
|
|
28
|
+
} from '@moostjs/event-wf'
|
|
29
|
+
|
|
30
|
+
// Register adapter
|
|
31
|
+
const wf = new MoostWf()
|
|
32
|
+
app.adapter(wf)
|
|
33
|
+
|
|
34
|
+
// Start / resume
|
|
35
|
+
const result = await wf.start('schema-id', initialContext, input?)
|
|
36
|
+
const resumed = await wf.resume(result.state, newInput)
|
|
37
|
+
|
|
38
|
+
// Observe
|
|
39
|
+
const detach = wf.attachSpy((event, output, flow, ms) => { ... })
|
|
40
|
+
```
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Core Concepts & Setup — @moostjs/event-wf
|
|
2
|
+
|
|
3
|
+
> Installation, mental model, adapter configuration, and getting started with Moost workflows.
|
|
4
|
+
|
|
5
|
+
## Concepts
|
|
6
|
+
|
|
7
|
+
Moost Workflows model multi-step processes as composable, typed sequences of operations. The key abstractions:
|
|
8
|
+
|
|
9
|
+
- **Steps** — Individual units of work. Controller methods decorated with `@Step` that read/write shared context.
|
|
10
|
+
- **Schemas** — Declarative arrays defining execution order, conditions, and loops.
|
|
11
|
+
- **Context** — A typed object that holds shared state across all steps in a workflow.
|
|
12
|
+
- **MoostWf** — The adapter class that bridges `@wooksjs/event-wf` into Moost's decorator/DI system.
|
|
13
|
+
- **TFlowOutput** — The result object from `start()`/`resume()` containing state, completion status, and resume functions.
|
|
14
|
+
|
|
15
|
+
Workflows are a good fit when your process has multiple stages, needs branching, can be interrupted for external input, requires auditability, or spans time.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @moostjs/event-wf
|
|
21
|
+
# or
|
|
22
|
+
pnpm add @moostjs/event-wf
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Peer dependencies: `moost`, `@wooksjs/event-core`, `@prostojs/infact`, `@prostojs/mate`, `wooks`.
|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
### 1. Define a workflow controller
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Controller, Injectable } from 'moost'
|
|
33
|
+
import { Step, Workflow, WorkflowParam, WorkflowSchema } from '@moostjs/event-wf'
|
|
34
|
+
|
|
35
|
+
interface TOrderContext {
|
|
36
|
+
orderId: string
|
|
37
|
+
validated: boolean
|
|
38
|
+
charged: boolean
|
|
39
|
+
shipped: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Injectable('FOR_EVENT')
|
|
43
|
+
@Controller()
|
|
44
|
+
export class OrderController {
|
|
45
|
+
@WorkflowParam('context')
|
|
46
|
+
ctx!: TOrderContext
|
|
47
|
+
|
|
48
|
+
@Workflow('process-order')
|
|
49
|
+
@WorkflowSchema<TOrderContext>(['validate', 'charge', 'ship'])
|
|
50
|
+
processOrder() {}
|
|
51
|
+
|
|
52
|
+
@Step('validate')
|
|
53
|
+
validate() {
|
|
54
|
+
this.ctx.validated = true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Step('charge')
|
|
58
|
+
charge() {
|
|
59
|
+
this.ctx.charged = true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Step('ship')
|
|
63
|
+
ship() {
|
|
64
|
+
this.ctx.shipped = true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Register the adapter and start
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { Moost } from 'moost'
|
|
73
|
+
import { MoostWf } from '@moostjs/event-wf'
|
|
74
|
+
import { OrderController } from './order.controller'
|
|
75
|
+
|
|
76
|
+
const app = new Moost()
|
|
77
|
+
const wf = new MoostWf()
|
|
78
|
+
|
|
79
|
+
app.adapter(wf)
|
|
80
|
+
app.registerControllers(OrderController)
|
|
81
|
+
await app.init()
|
|
82
|
+
|
|
83
|
+
const result = await wf.start('process-order', {
|
|
84
|
+
orderId: 'ORD-001',
|
|
85
|
+
validated: false,
|
|
86
|
+
charged: false,
|
|
87
|
+
shipped: false,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
console.log(result.finished) // true
|
|
91
|
+
console.log(result.state.context)
|
|
92
|
+
// { orderId: 'ORD-001', validated: true, charged: true, shipped: true }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## MoostWf Constructor
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
new MoostWf<T, IR>(opts?: WooksWf<T, IR> | TWooksWfOptions, debug?: boolean)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Parameter | Type | Description |
|
|
102
|
+
|-----------|------|-------------|
|
|
103
|
+
| `opts` | `WooksWf \| TWooksWfOptions \| undefined` | Pre-existing WooksWf instance, config options, or undefined for defaults |
|
|
104
|
+
| `debug` | `boolean` | Enable error logging |
|
|
105
|
+
|
|
106
|
+
**TWooksWfOptions fields:**
|
|
107
|
+
|
|
108
|
+
| Field | Type | Description |
|
|
109
|
+
|-------|------|-------------|
|
|
110
|
+
| `onError` | `(e: Error) => void` | Global error handler |
|
|
111
|
+
| `onNotFound` | `TWooksHandler` | Handler for unregistered steps |
|
|
112
|
+
| `onUnknownFlow` | `(schemaId: string, raiseError: () => void) => unknown` | Handler for unknown workflow schemas |
|
|
113
|
+
| `logger` | `TConsoleBase` | Custom logger instance |
|
|
114
|
+
| `eventOptions` | `EventContextOptions` | Event context configuration |
|
|
115
|
+
| `router` | `TWooksOptions['router']` | Router configuration |
|
|
116
|
+
|
|
117
|
+
You can also pass an existing `WooksWf` instance to share the workflow engine with non-Moost code:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const wooksWf = createWfApp()
|
|
121
|
+
const wf = new MoostWf(wooksWf)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Lifecycle Overview
|
|
125
|
+
|
|
126
|
+
1. `app.adapter(wf)` registers the adapter
|
|
127
|
+
2. `app.registerControllers(...)` registers controllers
|
|
128
|
+
3. `app.init()` processes decorators and binds `@Step`/`@Workflow` handlers
|
|
129
|
+
4. `wf.start(schemaId, context)` starts workflow execution
|
|
130
|
+
5. Steps execute in sequence, reading/mutating shared context
|
|
131
|
+
6. Workflow completes or pauses (if a step needs input)
|
|
132
|
+
|
|
133
|
+
## Best Practices
|
|
134
|
+
|
|
135
|
+
- Use `@Injectable('FOR_EVENT')` on workflow controllers to get fresh instances per step execution. Without it, the controller is a singleton — class properties would be shared across concurrent workflows.
|
|
136
|
+
- Keep the `@Workflow` entry point method body empty — all logic belongs in steps.
|
|
137
|
+
- Define a TypeScript interface for your context type and pass it as a generic to `@WorkflowSchema<T>()` for type-safe conditions.
|
|
138
|
+
- The workflow `state` object is plain JSON — you can serialize and store it for later resumption.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Decorators & Composables — @moostjs/event-wf
|
|
2
|
+
|
|
3
|
+
> All decorators and composables exported by @moostjs/event-wf for defining and accessing workflow handlers.
|
|
4
|
+
|
|
5
|
+
## API Reference
|
|
6
|
+
|
|
7
|
+
### `@Step(path?)`
|
|
8
|
+
|
|
9
|
+
Marks a controller method as a workflow step handler.
|
|
10
|
+
|
|
11
|
+
- **`path`** *(string, optional)* — Step identifier used in schemas. Defaults to the method name.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Step, WorkflowParam } from '@moostjs/event-wf'
|
|
15
|
+
|
|
16
|
+
@Step('validate')
|
|
17
|
+
validate(@WorkflowParam('context') ctx: TOrderContext) {
|
|
18
|
+
ctx.validated = true
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Steps can have parametric paths (like HTTP routes):
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Param } from 'moost'
|
|
26
|
+
|
|
27
|
+
@Step('notify/:channel(email|sms)')
|
|
28
|
+
notify(@Param('channel') channel: 'email' | 'sms') {
|
|
29
|
+
// Reference in schema: { id: 'notify/email' }
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `@Workflow(path?)`
|
|
34
|
+
|
|
35
|
+
Marks a controller method as a workflow entry point. Must be paired with `@WorkflowSchema`.
|
|
36
|
+
|
|
37
|
+
- **`path`** *(string, optional)* — Workflow identifier. Defaults to the method name.
|
|
38
|
+
|
|
39
|
+
The effective schema ID combines the controller prefix and the workflow path. For `@Controller('admin')` + `@Workflow('order')`, the schema ID is `admin/order`.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
@Workflow('order-processing')
|
|
43
|
+
@WorkflowSchema<TOrderContext>(['validate', 'charge', 'ship'])
|
|
44
|
+
processOrder() {}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `@WorkflowSchema<T>(schema)`
|
|
48
|
+
|
|
49
|
+
Attaches a workflow schema (step sequence) to a `@Workflow` method.
|
|
50
|
+
|
|
51
|
+
- **`T`** *(generic)* — Workflow context type for type-checked conditions.
|
|
52
|
+
- **`schema`** *(`TWorkflowSchema<T>`)* — Array of step definitions.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
@WorkflowSchema<TMyContext>([
|
|
56
|
+
'step-a',
|
|
57
|
+
{ condition: (ctx) => ctx.ready, id: 'step-b' },
|
|
58
|
+
{ while: 'retries < 3', steps: ['attempt', 'increment'] },
|
|
59
|
+
])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See [schemas.md](schemas.md) for full schema syntax.
|
|
63
|
+
|
|
64
|
+
### `WorkflowParam(name)`
|
|
65
|
+
|
|
66
|
+
Parameter and property decorator that injects workflow runtime values.
|
|
67
|
+
|
|
68
|
+
| Name | Type | Description |
|
|
69
|
+
|------|------|-------------|
|
|
70
|
+
| `'context'` | `T` | Shared workflow context object |
|
|
71
|
+
| `'input'` | `I \| undefined` | Input passed to `start()` or `resume()` |
|
|
72
|
+
| `'stepId'` | `string \| null` | Current step ID (`null` in entry point) |
|
|
73
|
+
| `'schemaId'` | `string` | Active workflow schema identifier |
|
|
74
|
+
| `'indexes'` | `number[] \| undefined` | Current position in nested schemas |
|
|
75
|
+
| `'resume'` | `boolean` | `true` when resuming, `false` on first run |
|
|
76
|
+
| `'state'` | `object` | Full state object from `useWfState()` |
|
|
77
|
+
|
|
78
|
+
**As method parameter:**
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
@Step('process')
|
|
82
|
+
process(
|
|
83
|
+
@WorkflowParam('context') ctx: TMyContext,
|
|
84
|
+
@WorkflowParam('input') input: TMyInput | undefined,
|
|
85
|
+
@WorkflowParam('resume') isResume: boolean,
|
|
86
|
+
) {
|
|
87
|
+
if (isResume) {
|
|
88
|
+
console.log('Resuming with new input:', input)
|
|
89
|
+
}
|
|
90
|
+
ctx.processed = true
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**As class property** (requires `@Injectable('FOR_EVENT')`):
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
@Injectable('FOR_EVENT')
|
|
98
|
+
@Controller()
|
|
99
|
+
export class MyController {
|
|
100
|
+
@WorkflowParam('context')
|
|
101
|
+
ctx!: TMyContext
|
|
102
|
+
|
|
103
|
+
@Step('review')
|
|
104
|
+
review() {
|
|
105
|
+
this.ctx.status = 'reviewed'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `useWfState()`
|
|
111
|
+
|
|
112
|
+
Composable that returns the current workflow execution state. Available inside step and entry point handlers.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { useWfState } from '@moostjs/event-wf'
|
|
116
|
+
|
|
117
|
+
const state = useWfState()
|
|
118
|
+
state.ctx<TMyContext>() // workflow context
|
|
119
|
+
state.input<TMyInput>() // step input (or undefined)
|
|
120
|
+
state.schemaId // workflow schema ID
|
|
121
|
+
state.stepId() // current step ID (or null)
|
|
122
|
+
state.indexes // position in nested schemas
|
|
123
|
+
state.resume // boolean: is this a resume?
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
In most cases, prefer `@WorkflowParam` decorators. The composable is useful for advanced scenarios like custom interceptors or utilities that need workflow context.
|
|
127
|
+
|
|
128
|
+
### `wfKind`
|
|
129
|
+
|
|
130
|
+
Event kind marker for workflow events. Used internally by the adapter and useful for building custom event-type-aware logic.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { wfKind } from '@moostjs/event-wf'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Common Patterns
|
|
137
|
+
|
|
138
|
+
### Pattern: Class Property Injection
|
|
139
|
+
|
|
140
|
+
When using `@Injectable('FOR_EVENT')`, inject context as a class property. Multiple steps in the same controller share the property declaration but get separate instances per execution.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
@Injectable('FOR_EVENT')
|
|
144
|
+
@Controller()
|
|
145
|
+
export class OnboardingController {
|
|
146
|
+
@WorkflowParam('context')
|
|
147
|
+
ctx!: TOnboardingContext
|
|
148
|
+
|
|
149
|
+
@Step('verify-email')
|
|
150
|
+
verifyEmail() { this.ctx.emailVerified = true }
|
|
151
|
+
|
|
152
|
+
@Step('collect-profile')
|
|
153
|
+
collectProfile() { this.ctx.profileComplete = true }
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Pattern: Reusing Steps Across Workflows
|
|
158
|
+
|
|
159
|
+
Steps are resolved by their path through the Wooks router. Any workflow schema can reference any registered step:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
@Controller()
|
|
163
|
+
export class SharedSteps {
|
|
164
|
+
@Step('send-notification')
|
|
165
|
+
sendNotification(@WorkflowParam('context') ctx: { email: string, message: string }) {
|
|
166
|
+
sendEmail(ctx.email, ctx.message)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Referenced from multiple schemas:
|
|
171
|
+
@WorkflowSchema(['validate', 'process', 'send-notification'])
|
|
172
|
+
@WorkflowSchema(['review', 'approve', 'send-notification'])
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Gotchas
|
|
176
|
+
|
|
177
|
+
- `WorkflowParam('resume')` returns a **boolean**, not a function. It indicates whether the current execution is a resume. The resume *function* is on the `TFlowOutput` object returned by `start()` or `resume()`.
|
|
178
|
+
- Without `@Injectable('FOR_EVENT')`, the controller is a singleton. Using class property injection in a singleton causes state to leak between concurrent workflows — use method parameters instead.
|
|
179
|
+
- If you omit the `path` argument from `@Step()` or `@Workflow()`, the method name is used as the identifier.
|