yaml-flow 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +486 -255
- package/dist/constants-D1fTEbbM.d.cts +330 -0
- package/dist/constants-D1fTEbbM.d.ts +330 -0
- package/dist/event-graph/index.cjs +895 -0
- package/dist/event-graph/index.cjs.map +1 -0
- package/dist/event-graph/index.d.cts +53 -0
- package/dist/event-graph/index.d.ts +53 -0
- package/dist/event-graph/index.js +855 -0
- package/dist/event-graph/index.js.map +1 -0
- package/dist/index.cjs +1128 -312
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1093 -306
- package/dist/index.js.map +1 -1
- package/dist/step-machine/index.cjs +513 -0
- package/dist/step-machine/index.cjs.map +1 -0
- package/dist/step-machine/index.d.cts +77 -0
- package/dist/step-machine/index.d.ts +77 -0
- package/dist/step-machine/index.js +502 -0
- package/dist/step-machine/index.js.map +1 -0
- package/dist/stores/file.cjs.map +1 -1
- package/dist/stores/file.d.cts +4 -4
- package/dist/stores/file.d.ts +4 -4
- package/dist/stores/file.js.map +1 -1
- package/dist/stores/index.cjs +232 -0
- package/dist/stores/index.cjs.map +1 -0
- package/dist/stores/index.d.cts +4 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +228 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/stores/localStorage.cjs.map +1 -1
- package/dist/stores/localStorage.d.cts +4 -4
- package/dist/stores/localStorage.d.ts +4 -4
- package/dist/stores/localStorage.js.map +1 -1
- package/dist/stores/memory.cjs.map +1 -1
- package/dist/stores/memory.d.cts +4 -4
- package/dist/stores/memory.d.ts +4 -4
- package/dist/stores/memory.js.map +1 -1
- package/dist/types-FZ_eyErS.d.cts +115 -0
- package/dist/types-FZ_eyErS.d.ts +115 -0
- package/package.json +16 -6
- package/dist/core/index.cjs +0 -557
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -102
- package/dist/core/index.d.ts +0 -102
- package/dist/core/index.js +0 -549
- package/dist/core/index.js.map +0 -1
- package/dist/types-BoWndaAJ.d.cts +0 -237
- package/dist/types-BoWndaAJ.d.ts +0 -237
package/README.md
CHANGED
|
@@ -1,379 +1,610 @@
|
|
|
1
1
|
# yaml-flow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two workflow engines in one package. Pick the model that fits your problem.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/yaml-flow)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
```
|
|
9
|
+
npm install yaml-flow
|
|
10
|
+
```
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
- **Declarative Flows** — Define workflows in YAML with JSON Schema validation
|
|
12
|
-
- **Pure Function Handlers** — Step handlers are simple `(input, context) => result` functions
|
|
13
|
-
- **Pluggable Storage** — Bring your own persistence (memory, localStorage, file, Redis, etc.)
|
|
14
|
-
- **Zero Core Dependencies** — Lightweight core, optional add-ons for YAML parsing
|
|
15
|
-
- **Resumable** — Pause and resume flows from persisted state
|
|
16
|
-
- **Circuit Breakers** — Prevent infinite loops with configurable limits
|
|
17
|
-
- **Retry Logic** — Built-in exponential backoff for failed steps
|
|
18
|
-
- **Event System** — Subscribe to flow events for UI updates
|
|
12
|
+
## Which Mode Do I Need?
|
|
19
13
|
|
|
20
|
-
|
|
14
|
+
yaml-flow ships two execution models. They solve fundamentally different problems.
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
| | **Step Machine** | **Event Graph** |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| **Mental model** | Flowchart — each step decides what runs next | Dependency graph — tasks self-select when their inputs are ready |
|
|
19
|
+
| **Core function** | `applyStepResult(flow, state, step, result) → newState` | `next(graph, state) → eligibleTasks` + `apply(state, event, graph) → newState` |
|
|
20
|
+
| **Config shape** | `steps:` with `transitions:` mapping | `tasks:` with `requires:` / `provides:` arrays |
|
|
21
|
+
| **Who decides next?** | The sender (current step's result picks the transition) | The receivers (tasks become eligible when their required tokens appear) |
|
|
22
|
+
| **Has a driver?** | Yes — `StepMachine` class runs the loop for you | No — you call `next()` and `apply()` yourself (or write a 10-line loop) |
|
|
23
|
+
| **Import** | `import { StepMachine } from 'yaml-flow/step-machine'` | `import { next, apply } from 'yaml-flow/event-graph'` |
|
|
24
|
+
|
|
25
|
+
### Use Step Machine when…
|
|
26
|
+
|
|
27
|
+
- Your workflow is a **known sequence** of steps with conditional branching.
|
|
28
|
+
- You need **pause/resume** built in.
|
|
29
|
+
- The work is conversational: receive input → process → decide next step → repeat.
|
|
30
|
+
- You want to hand the library a YAML file and some handler functions and call `.run()`.
|
|
31
|
+
|
|
32
|
+
**Examples:** form wizards, approval chains, AI chat loops, order processing pipelines, ETL with linear stages.
|
|
33
|
+
|
|
34
|
+
### Use Event Graph when…
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
- Tasks have **complex dependencies** — diamonds, fan-out, fan-in, optional paths.
|
|
37
|
+
- You don't know the execution order up front; it **emerges from the data**.
|
|
38
|
+
- Multiple tasks can run **in parallel** once their inputs are satisfied.
|
|
39
|
+
- You need **conflict resolution** when two tasks compete to produce the same output.
|
|
40
|
+
- Your system is **event-driven**: external events inject tokens that unblock downstream tasks.
|
|
41
|
+
- You want the engine to be a **pure function** you embed in your own scheduler, serverless function, or agent loop.
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
**Examples:** CI/CD pipelines, agent tool orchestration, research workflows (fetch → analyse A | analyse B → merge), build systems, multi-model AI routing, eligibility engines.
|
|
44
|
+
|
|
45
|
+
### When in doubt
|
|
46
|
+
|
|
47
|
+
Start with **Step Machine** if your workflow diagram is a straight line with branches.
|
|
48
|
+
Start with **Event Graph** if your workflow diagram has diamonds or parallel lanes.
|
|
49
|
+
|
|
50
|
+
Both are pure `f(state, input) → newState` at their core. You can always call the reducer directly without the driver class.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Step Machine — Quick Start
|
|
55
|
+
|
|
56
|
+
### 1. Define a flow
|
|
29
57
|
|
|
30
58
|
```yaml
|
|
31
|
-
#
|
|
59
|
+
# support-ticket.yaml
|
|
32
60
|
settings:
|
|
33
|
-
start_step:
|
|
34
|
-
max_total_steps:
|
|
61
|
+
start_step: classify
|
|
62
|
+
max_total_steps: 20
|
|
35
63
|
|
|
36
64
|
steps:
|
|
37
|
-
|
|
38
|
-
produces_data:
|
|
39
|
-
|
|
65
|
+
classify:
|
|
66
|
+
produces_data: [category, priority]
|
|
67
|
+
transitions:
|
|
68
|
+
billing: handle_billing
|
|
69
|
+
technical: handle_technical
|
|
70
|
+
general: handle_general
|
|
71
|
+
|
|
72
|
+
handle_billing:
|
|
73
|
+
expects_data: [category]
|
|
74
|
+
produces_data: [resolution]
|
|
75
|
+
transitions:
|
|
76
|
+
resolved: close_ticket
|
|
77
|
+
escalate: escalate_ticket
|
|
78
|
+
|
|
79
|
+
handle_technical:
|
|
80
|
+
expects_data: [category]
|
|
81
|
+
produces_data: [resolution]
|
|
40
82
|
transitions:
|
|
41
|
-
|
|
42
|
-
|
|
83
|
+
resolved: close_ticket
|
|
84
|
+
escalate: escalate_ticket
|
|
85
|
+
retry:
|
|
86
|
+
max_attempts: 2
|
|
87
|
+
delay_ms: 1000
|
|
88
|
+
|
|
89
|
+
handle_general:
|
|
90
|
+
expects_data: [category]
|
|
91
|
+
produces_data: [resolution]
|
|
92
|
+
transitions:
|
|
93
|
+
resolved: close_ticket
|
|
94
|
+
|
|
95
|
+
escalate_ticket:
|
|
96
|
+
expects_data: [category, priority]
|
|
97
|
+
produces_data: [escalation_id]
|
|
98
|
+
transitions:
|
|
99
|
+
done: close_ticket
|
|
43
100
|
|
|
44
101
|
terminal_states:
|
|
45
|
-
|
|
46
|
-
return_intent:
|
|
47
|
-
return_artifacts:
|
|
48
|
-
|
|
49
|
-
error:
|
|
50
|
-
return_intent: error
|
|
51
|
-
return_artifacts: false
|
|
102
|
+
close_ticket:
|
|
103
|
+
return_intent: resolved
|
|
104
|
+
return_artifacts: [resolution]
|
|
52
105
|
```
|
|
53
106
|
|
|
54
|
-
### 2.
|
|
107
|
+
### 2. Write handlers and run
|
|
55
108
|
|
|
56
109
|
```typescript
|
|
57
|
-
import {
|
|
110
|
+
import { createStepMachine, loadStepFlow } from 'yaml-flow/step-machine';
|
|
111
|
+
import { MemoryStore } from 'yaml-flow/stores/memory';
|
|
112
|
+
|
|
113
|
+
const flow = await loadStepFlow('./support-ticket.yaml');
|
|
58
114
|
|
|
59
115
|
const handlers = {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
116
|
+
classify: async (input) => {
|
|
117
|
+
const category = detectCategory(input.message);
|
|
118
|
+
return { result: category, data: { category, priority: 'high' } };
|
|
119
|
+
},
|
|
120
|
+
handle_billing: async (input, ctx) => {
|
|
121
|
+
const answer = await ctx.components.ai.ask(`Billing issue: ${input.category}`);
|
|
122
|
+
return { result: 'resolved', data: { resolution: answer } };
|
|
123
|
+
},
|
|
124
|
+
handle_technical: async (input, ctx) => {
|
|
125
|
+
const answer = await ctx.components.ai.ask(`Tech issue: ${input.category}`);
|
|
126
|
+
return answer.confidence > 0.7
|
|
127
|
+
? { result: 'resolved', data: { resolution: answer.text } }
|
|
128
|
+
: { result: 'escalate' };
|
|
129
|
+
},
|
|
130
|
+
handle_general: async (input) => {
|
|
131
|
+
return { result: 'resolved', data: { resolution: 'See FAQ.' } };
|
|
132
|
+
},
|
|
133
|
+
escalate_ticket: async (input, ctx) => {
|
|
134
|
+
const id = await ctx.components.ticketSystem.escalate(input.category);
|
|
135
|
+
return { result: 'done', data: { escalation_id: id } };
|
|
136
|
+
},
|
|
66
137
|
};
|
|
138
|
+
|
|
139
|
+
const machine = createStepMachine(flow, handlers, {
|
|
140
|
+
store: new MemoryStore(),
|
|
141
|
+
components: { ai: myAIClient, ticketSystem: myTicketAPI },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = await machine.run({ message: 'I was double-charged' });
|
|
145
|
+
// result.intent → 'resolved'
|
|
146
|
+
// result.data → { resolution: '...' }
|
|
147
|
+
// result.stepHistory → ['classify', 'handle_billing']
|
|
67
148
|
```
|
|
68
149
|
|
|
69
|
-
###
|
|
150
|
+
### Step Machine features at a glance
|
|
151
|
+
|
|
152
|
+
| Feature | Config |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Transitions | `transitions: { success: next_step, failure: error_step }` |
|
|
155
|
+
| Retry | `retry: { max_attempts: 3, delay_ms: 1000, backoff_multiplier: 2 }` |
|
|
156
|
+
| Circuit breaker | `circuit_breaker: { max_iterations: 5, on_open: fallback }` |
|
|
157
|
+
| Pause / resume | `await machine.pause(runId)` / `await machine.resume(runId)` |
|
|
158
|
+
| Cancellation | Pass `signal: AbortController.signal` in options |
|
|
159
|
+
| Events | `machine.on('step:complete', fn)` — also `flow:start`, `flow:complete`, `transition`, etc. |
|
|
160
|
+
| Data flow | `expects_data` filters what a handler receives; `produces_data` documents what it returns |
|
|
161
|
+
|
|
162
|
+
### Using the pure reducer directly (no driver)
|
|
70
163
|
|
|
71
164
|
```typescript
|
|
72
|
-
|
|
73
|
-
|
|
165
|
+
import { createInitialState, applyStepResult, checkCircuitBreaker, computeStepInput } from 'yaml-flow/step-machine';
|
|
166
|
+
|
|
167
|
+
let state = createInitialState(flow, 'run-1');
|
|
74
168
|
|
|
75
|
-
|
|
76
|
-
|
|
169
|
+
// Your own loop
|
|
170
|
+
while (true) {
|
|
171
|
+
const cb = checkCircuitBreaker(flow, state, state.currentStep);
|
|
172
|
+
if (cb.broken) { state = cb.newState; continue; }
|
|
173
|
+
state = cb.newState;
|
|
174
|
+
|
|
175
|
+
const input = computeStepInput(flow, state.currentStep, allData);
|
|
176
|
+
const stepResult = await handlers[state.currentStep](input, context);
|
|
177
|
+
const { newState, isTerminal } = applyStepResult(flow, state, state.currentStep, stepResult);
|
|
178
|
+
state = newState;
|
|
179
|
+
if (isTerminal) break;
|
|
180
|
+
}
|
|
77
181
|
```
|
|
78
182
|
|
|
79
|
-
|
|
183
|
+
---
|
|
80
184
|
|
|
81
|
-
|
|
185
|
+
## Event Graph — Quick Start
|
|
186
|
+
|
|
187
|
+
The event graph has no driver class. You call two pure functions in a loop:
|
|
188
|
+
|
|
189
|
+
- **`next(graph, state)`** → tells you which tasks are eligible right now
|
|
190
|
+
- **`apply(state, event, graph)`** → applies an event and returns the new state
|
|
191
|
+
|
|
192
|
+
You decide how to actually execute the tasks (call an API, run a function, ask an LLM, etc.).
|
|
193
|
+
|
|
194
|
+
### 1. Define a graph
|
|
82
195
|
|
|
83
196
|
```yaml
|
|
197
|
+
# research-pipeline.yaml
|
|
84
198
|
settings:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
```
|
|
199
|
+
completion: goal-reached
|
|
200
|
+
goal: [final-report]
|
|
201
|
+
conflict_strategy: parallel-all
|
|
89
202
|
|
|
90
|
-
|
|
203
|
+
tasks:
|
|
204
|
+
fetch_sources:
|
|
205
|
+
provides: [raw-sources]
|
|
91
206
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
description: "What this step does"
|
|
96
|
-
expects_data: # Input data keys
|
|
97
|
-
- input_key
|
|
98
|
-
produces_data: # Output data keys
|
|
99
|
-
- output_key
|
|
100
|
-
transitions: # Result -> next step mapping
|
|
101
|
-
success: next_step
|
|
102
|
-
failure: error_step
|
|
103
|
-
retry: # Optional retry config
|
|
104
|
-
max_attempts: 3
|
|
105
|
-
delay_ms: 1000
|
|
106
|
-
backoff_multiplier: 2
|
|
107
|
-
circuit_breaker: # Optional loop protection
|
|
108
|
-
max_iterations: 5
|
|
109
|
-
on_open: fallback_step
|
|
110
|
-
```
|
|
207
|
+
analyse_sentiment:
|
|
208
|
+
requires: [raw-sources]
|
|
209
|
+
provides: [sentiment-result]
|
|
111
210
|
|
|
112
|
-
|
|
211
|
+
analyse_entities:
|
|
212
|
+
requires: [raw-sources]
|
|
213
|
+
provides: [entity-result]
|
|
113
214
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
error:
|
|
123
|
-
return_intent: "error"
|
|
124
|
-
return_artifacts: false
|
|
215
|
+
merge_analysis:
|
|
216
|
+
requires: [sentiment-result, entity-result]
|
|
217
|
+
provides: [merged-analysis]
|
|
218
|
+
|
|
219
|
+
generate_report:
|
|
220
|
+
requires: [merged-analysis]
|
|
221
|
+
provides: [final-report]
|
|
125
222
|
```
|
|
126
223
|
|
|
127
|
-
|
|
224
|
+
`fetch_sources` runs first (no requires). Once it completes, both `analyse_sentiment` and `analyse_entities` become eligible in parallel. `merge_analysis` waits for both. `generate_report` waits for the merge. Done when `final-report` appears.
|
|
128
225
|
|
|
129
|
-
|
|
226
|
+
### 2. Write a driver loop
|
|
130
227
|
|
|
131
228
|
```typescript
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
[key: string]: unknown; // Data from expects_data
|
|
136
|
-
}
|
|
229
|
+
import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
|
|
230
|
+
import { parse } from 'yaml';
|
|
231
|
+
import { readFileSync } from 'fs';
|
|
137
232
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
stepName: string; // Current step name
|
|
141
|
-
components: object; // Injected dependencies
|
|
142
|
-
store: FlowStore; // Direct store access
|
|
143
|
-
signal?: AbortSignal; // Cancellation signal
|
|
144
|
-
emit: (event, data) => void; // Event emitter
|
|
145
|
-
}
|
|
233
|
+
const graph = parse(readFileSync('./research-pipeline.yaml', 'utf8'));
|
|
234
|
+
let state = createInitialExecutionState(graph, 'exec-1');
|
|
146
235
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
data?: object; // Output data (matches produces_data)
|
|
150
|
-
}
|
|
151
|
-
```
|
|
236
|
+
while (true) {
|
|
237
|
+
const schedule = next(graph, state);
|
|
152
238
|
|
|
153
|
-
|
|
239
|
+
if (schedule.isComplete) {
|
|
240
|
+
console.log('Done!', state.availableOutputs);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (schedule.stuckDetection.is_stuck) {
|
|
244
|
+
console.error('Stuck:', schedule.stuckDetection.stuck_description);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
154
247
|
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
result: 'failure',
|
|
176
|
-
data: { error: error.message }
|
|
177
|
-
};
|
|
248
|
+
// Execute all eligible tasks (in parallel)
|
|
249
|
+
const results = await Promise.all(
|
|
250
|
+
schedule.eligibleTasks.map(async (taskName) => {
|
|
251
|
+
state = apply(state, { type: 'task-started', taskName, timestamp: new Date().toISOString() }, graph);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const output = await executeTask(taskName, state);
|
|
255
|
+
return { taskName, success: true, result: output };
|
|
256
|
+
} catch (err) {
|
|
257
|
+
return { taskName, success: false, error: err.message };
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Feed results back into the reducer
|
|
263
|
+
for (const r of results) {
|
|
264
|
+
if (r.success) {
|
|
265
|
+
state = apply(state, { type: 'task-completed', taskName: r.taskName, result: r.result, timestamp: new Date().toISOString() }, graph);
|
|
266
|
+
} else {
|
|
267
|
+
state = apply(state, { type: 'task-failed', taskName: r.taskName, error: r.error, timestamp: new Date().toISOString() }, graph);
|
|
178
268
|
}
|
|
179
269
|
}
|
|
180
|
-
}
|
|
270
|
+
}
|
|
181
271
|
```
|
|
182
272
|
|
|
183
|
-
|
|
273
|
+
That's the entire integration. ~30 lines. The engine is pure; your loop owns the I/O.
|
|
184
274
|
|
|
185
|
-
###
|
|
275
|
+
### Event Graph features at a glance
|
|
186
276
|
|
|
187
|
-
|
|
188
|
-
|
|
277
|
+
| Feature | Config | What it does |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| Dependencies | `requires: [a, b]` / `provides: [c]` | Task runs when all required tokens are available |
|
|
280
|
+
| Conditional routing | `on: { positive: [pos-result], negative: [neg-result] }` | Different outputs based on task result |
|
|
281
|
+
| Failure tokens | `on_failure: [data-unavailable]` | Inject tokens on failure so downstream alternatives can activate |
|
|
282
|
+
| Retry | `retry: { max_attempts: 3 }` | Auto-retry on failure (task resets to not-started) |
|
|
283
|
+
| Repeatable tasks | `repeatable: true` or `repeatable: { max: 5 }` | Task can re-execute when its inputs refresh |
|
|
284
|
+
| Circuit breaker | `circuit_breaker: { max_executions: 10, on_break: [stop-token] }` | Inject tokens after N executions |
|
|
285
|
+
| External events | `apply(state, { type: 'inject-tokens', tokens: ['user-approved'] })` | Unblock tasks waiting on external input |
|
|
286
|
+
| Dynamic tasks | `apply(state, { type: 'task-creation', taskName: 'new', taskConfig: {...} })` | Add tasks at runtime |
|
|
287
|
+
| Completion strategies | `completion: all-tasks-done \| all-outputs-done \| goal-reached \| manual` | When is the graph "done"? |
|
|
288
|
+
| Conflict resolution | `conflict_strategy: alphabetical \| priority-first \| parallel-all \| ...` | What happens when two tasks produce the same output? |
|
|
289
|
+
| Stuck detection | Automatic via `next()` | Returns `is_stuck: true` with description when no progress is possible |
|
|
189
290
|
|
|
190
|
-
|
|
191
|
-
store: new MemoryStore()
|
|
192
|
-
});
|
|
193
|
-
```
|
|
291
|
+
### Completion strategies explained
|
|
194
292
|
|
|
195
|
-
|
|
293
|
+
| Strategy | Meaning |
|
|
294
|
+
|---|---|
|
|
295
|
+
| `all-tasks-done` | Every task has completed (or failed/inactivated) |
|
|
296
|
+
| `all-outputs-done` | Every `provides` token from every task is available |
|
|
297
|
+
| `goal-reached` | Specific tokens listed in `settings.goal` are available |
|
|
298
|
+
| `only-resolved` | All non-failed tasks have completed |
|
|
299
|
+
| `manual` | Never auto-completes; you decide when to stop |
|
|
196
300
|
|
|
197
|
-
|
|
198
|
-
import { LocalStorageStore } from 'yaml-flow/stores/localStorage';
|
|
301
|
+
### Conflict resolution strategies
|
|
199
302
|
|
|
200
|
-
|
|
201
|
-
store: new LocalStorageStore({ prefix: 'myapp' })
|
|
202
|
-
});
|
|
203
|
-
```
|
|
303
|
+
When multiple eligible tasks produce the same output token, only one should run (unless you want parallel-all). The `conflict_strategy` setting controls the selection:
|
|
204
304
|
|
|
205
|
-
|
|
305
|
+
| Strategy | Behaviour |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `alphabetical` | Pick the alphabetically first task name (default, deterministic) |
|
|
308
|
+
| `priority-first` | Pick the task with the highest `priority` value |
|
|
309
|
+
| `duration-first` | Pick the task with the lowest `estimatedDuration` |
|
|
310
|
+
| `cost-optimized` | Pick the task with the lowest `estimatedCost` |
|
|
311
|
+
| `resource-aware` | Pick the task with the lowest total resource requirements |
|
|
312
|
+
| `round-robin` | Rotate among competing tasks across scheduler calls |
|
|
313
|
+
| `random-select` | Pick one at random |
|
|
314
|
+
| `parallel-all` | Run all competing tasks (no conflict resolution) |
|
|
315
|
+
| `user-choice` | Return all candidates; let the caller decide |
|
|
316
|
+
| `skip-conflicts` | Skip all tasks involved in a conflict |
|
|
206
317
|
|
|
207
|
-
|
|
208
|
-
import { FileStore } from 'yaml-flow/stores/file';
|
|
318
|
+
---
|
|
209
319
|
|
|
210
|
-
|
|
211
|
-
store: new FileStore({ directory: './flow-data' })
|
|
212
|
-
});
|
|
213
|
-
```
|
|
320
|
+
## Practical Patterns
|
|
214
321
|
|
|
215
|
-
###
|
|
322
|
+
### Pattern: AI Agent Tool Orchestration (Event Graph)
|
|
216
323
|
|
|
217
|
-
|
|
324
|
+
An agent needs to gather evidence from multiple sources, then synthesize.
|
|
218
325
|
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
326
|
+
```yaml
|
|
327
|
+
settings:
|
|
328
|
+
completion: goal-reached
|
|
329
|
+
goal: [final-answer]
|
|
330
|
+
conflict_strategy: parallel-all
|
|
331
|
+
|
|
332
|
+
tasks:
|
|
333
|
+
search_web:
|
|
334
|
+
provides: [web-results]
|
|
335
|
+
search_database:
|
|
336
|
+
provides: [db-results]
|
|
337
|
+
search_documents:
|
|
338
|
+
provides: [doc-results]
|
|
339
|
+
|
|
340
|
+
synthesize:
|
|
341
|
+
requires: [web-results, db-results, doc-results]
|
|
342
|
+
provides: [draft-answer]
|
|
343
|
+
|
|
344
|
+
verify:
|
|
345
|
+
requires: [draft-answer]
|
|
346
|
+
provides: [final-answer]
|
|
347
|
+
on:
|
|
348
|
+
verified: [final-answer]
|
|
349
|
+
rejected: [needs-revision]
|
|
350
|
+
on_failure: [verification-skipped]
|
|
351
|
+
|
|
352
|
+
revise:
|
|
353
|
+
requires: [needs-revision]
|
|
354
|
+
provides: [draft-answer]
|
|
355
|
+
repeatable: { max: 3 }
|
|
229
356
|
```
|
|
230
357
|
|
|
231
|
-
|
|
358
|
+
The three searches run in parallel. `synthesize` waits for all three. `verify` can produce different token sets depending on its result. If rejected, `revise` picks up and feeds back into `verify` (up to 3 times). If verify itself fails, `verification-skipped` unblocks any downstream task waiting on it.
|
|
232
359
|
|
|
233
|
-
|
|
360
|
+
### Pattern: Order Processing Pipeline (Step Machine)
|
|
234
361
|
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
api: httpClient,
|
|
240
|
-
cache: redisClient,
|
|
241
|
-
ai: openAIClient
|
|
242
|
-
}
|
|
243
|
-
});
|
|
362
|
+
```yaml
|
|
363
|
+
settings:
|
|
364
|
+
start_step: validate
|
|
365
|
+
max_total_steps: 15
|
|
244
366
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
};
|
|
252
|
-
```
|
|
367
|
+
steps:
|
|
368
|
+
validate:
|
|
369
|
+
produces_data: [validated_order]
|
|
370
|
+
transitions:
|
|
371
|
+
valid: charge_payment
|
|
372
|
+
invalid: reject
|
|
253
373
|
|
|
254
|
-
|
|
374
|
+
charge_payment:
|
|
375
|
+
expects_data: [validated_order]
|
|
376
|
+
produces_data: [payment_id]
|
|
377
|
+
transitions:
|
|
378
|
+
success: ship
|
|
379
|
+
declined: reject
|
|
380
|
+
retry:
|
|
381
|
+
max_attempts: 3
|
|
382
|
+
delay_ms: 2000
|
|
383
|
+
backoff_multiplier: 2
|
|
255
384
|
|
|
256
|
-
|
|
385
|
+
ship:
|
|
386
|
+
expects_data: [validated_order, payment_id]
|
|
387
|
+
produces_data: [tracking_number]
|
|
388
|
+
transitions:
|
|
389
|
+
shipped: confirm
|
|
390
|
+
failure: refund
|
|
257
391
|
|
|
258
|
-
|
|
259
|
-
|
|
392
|
+
refund:
|
|
393
|
+
expects_data: [payment_id]
|
|
394
|
+
produces_data: [refund_id]
|
|
395
|
+
transitions:
|
|
396
|
+
done: reject
|
|
260
397
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
398
|
+
confirm:
|
|
399
|
+
expects_data: [tracking_number]
|
|
400
|
+
transitions:
|
|
401
|
+
done: complete
|
|
265
402
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
403
|
+
terminal_states:
|
|
404
|
+
complete:
|
|
405
|
+
return_intent: success
|
|
406
|
+
return_artifacts: [tracking_number, payment_id]
|
|
407
|
+
reject:
|
|
408
|
+
return_intent: rejected
|
|
409
|
+
return_artifacts: false
|
|
270
410
|
```
|
|
271
411
|
|
|
272
|
-
|
|
412
|
+
Linear with branches. The current step decides what's next. Retry on payment failures with exponential backoff.
|
|
413
|
+
|
|
414
|
+
### Pattern: Inject External Events (Event Graph)
|
|
273
415
|
|
|
274
416
|
```typescript
|
|
275
|
-
//
|
|
276
|
-
const
|
|
417
|
+
// A task is waiting for human approval
|
|
418
|
+
const graph = {
|
|
419
|
+
settings: { completion: 'goal-reached', goal: ['deployed'] },
|
|
420
|
+
tasks: {
|
|
421
|
+
build: { provides: ['build-artifact'] },
|
|
422
|
+
test: { requires: ['build-artifact'], provides: ['test-passed'] },
|
|
423
|
+
approve: { requires: ['test-passed', 'human-approval'], provides: ['approved'] },
|
|
424
|
+
deploy: { requires: ['approved'], provides: ['deployed'] },
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
let state = createInitialExecutionState(graph, 'deploy-1');
|
|
429
|
+
|
|
430
|
+
// ... run build and test normally ...
|
|
277
431
|
|
|
278
|
-
//
|
|
279
|
-
|
|
432
|
+
// Later, when a human clicks "Approve" in your UI:
|
|
433
|
+
state = apply(state, {
|
|
434
|
+
type: 'inject-tokens',
|
|
435
|
+
tokens: ['human-approval'],
|
|
436
|
+
timestamp: new Date().toISOString(),
|
|
437
|
+
}, graph);
|
|
280
438
|
|
|
281
|
-
//
|
|
282
|
-
const resumed = await engine.resume(runId);
|
|
439
|
+
// Now next(graph, state) will return 'approve' as eligible
|
|
283
440
|
```
|
|
284
441
|
|
|
285
|
-
|
|
442
|
+
### Pattern: Conditional Branching in Event Graph
|
|
286
443
|
|
|
287
|
-
```
|
|
288
|
-
|
|
444
|
+
```yaml
|
|
445
|
+
tasks:
|
|
446
|
+
classify_image:
|
|
447
|
+
provides: [classification]
|
|
448
|
+
on:
|
|
449
|
+
photo: [is-photo]
|
|
450
|
+
document: [is-document]
|
|
451
|
+
screenshot: [is-screenshot]
|
|
452
|
+
|
|
453
|
+
enhance_photo:
|
|
454
|
+
requires: [is-photo]
|
|
455
|
+
provides: [processed-image]
|
|
456
|
+
|
|
457
|
+
ocr_document:
|
|
458
|
+
requires: [is-document]
|
|
459
|
+
provides: [extracted-text]
|
|
460
|
+
|
|
461
|
+
crop_screenshot:
|
|
462
|
+
requires: [is-screenshot]
|
|
463
|
+
provides: [processed-image]
|
|
464
|
+
```
|
|
289
465
|
|
|
290
|
-
|
|
291
|
-
signal: controller.signal
|
|
292
|
-
});
|
|
466
|
+
Only one downstream path activates based on the classifier result. This is the event-graph equivalent of a switch statement.
|
|
293
467
|
|
|
294
|
-
|
|
295
|
-
const resultPromise = engine.run();
|
|
468
|
+
---
|
|
296
469
|
|
|
297
|
-
|
|
298
|
-
controller.abort();
|
|
299
|
-
```
|
|
470
|
+
## Storage Adapters
|
|
300
471
|
|
|
301
|
-
|
|
472
|
+
All three stores work with both modes. Step Machine uses them for run state persistence. Event Graph state is plain JSON — serialize it yourself or use a store.
|
|
302
473
|
|
|
303
|
-
###
|
|
474
|
+
### Memory (default, all environments)
|
|
304
475
|
|
|
305
476
|
```typescript
|
|
306
|
-
import {
|
|
477
|
+
import { MemoryStore } from 'yaml-flow/stores/memory';
|
|
478
|
+
```
|
|
307
479
|
|
|
308
|
-
|
|
309
|
-
const flow = {
|
|
310
|
-
settings: { start_step: 'start' },
|
|
311
|
-
steps: { start: { transitions: { success: 'done' } } },
|
|
312
|
-
terminal_states: { done: { return_intent: 'success' } }
|
|
313
|
-
};
|
|
480
|
+
### LocalStorage (browser)
|
|
314
481
|
|
|
315
|
-
|
|
482
|
+
```typescript
|
|
483
|
+
import { LocalStorageStore } from 'yaml-flow/stores/localStorage';
|
|
484
|
+
new LocalStorageStore({ prefix: 'myapp' });
|
|
316
485
|
```
|
|
317
486
|
|
|
318
|
-
###
|
|
487
|
+
### File System (Node.js)
|
|
319
488
|
|
|
320
489
|
```typescript
|
|
321
|
-
import {
|
|
322
|
-
|
|
323
|
-
const flow = await loadFlowFromUrl('/flows/my-flow.json');
|
|
324
|
-
const engine = createEngine(flow, handlers);
|
|
490
|
+
import { FileStore } from 'yaml-flow/stores/file';
|
|
491
|
+
new FileStore({ directory: './flow-data' });
|
|
325
492
|
```
|
|
326
493
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
Use the included JSON Schema for IDE autocomplete:
|
|
494
|
+
### Custom Store
|
|
330
495
|
|
|
331
|
-
|
|
332
|
-
# yaml-language-server: $schema=node_modules/yaml-flow/schema/flow.schema.json
|
|
496
|
+
Implement the `StepMachineStore` interface:
|
|
333
497
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
498
|
+
```typescript
|
|
499
|
+
interface StepMachineStore {
|
|
500
|
+
saveRunState(runId: string, state: StepMachineState): Promise<void>;
|
|
501
|
+
loadRunState(runId: string): Promise<StepMachineState | null>;
|
|
502
|
+
deleteRunState(runId: string): Promise<void>;
|
|
503
|
+
setData(runId: string, key: string, value: unknown): Promise<void>;
|
|
504
|
+
getData(runId: string, key: string): Promise<unknown>;
|
|
505
|
+
getAllData(runId: string): Promise<Record<string, unknown>>;
|
|
506
|
+
clearData(runId: string): Promise<void>;
|
|
507
|
+
}
|
|
337
508
|
```
|
|
338
509
|
|
|
339
|
-
|
|
510
|
+
---
|
|
340
511
|
|
|
341
|
-
|
|
512
|
+
## Package Exports
|
|
342
513
|
|
|
343
|
-
|
|
514
|
+
```typescript
|
|
515
|
+
// Everything (both modes + stores)
|
|
516
|
+
import { StepMachine, next, apply, MemoryStore } from 'yaml-flow';
|
|
344
517
|
|
|
345
|
-
|
|
518
|
+
// Step Machine only
|
|
519
|
+
import { StepMachine, createStepMachine, loadStepFlow } from 'yaml-flow/step-machine';
|
|
520
|
+
import { applyStepResult, checkCircuitBreaker, createInitialState } from 'yaml-flow/step-machine';
|
|
346
521
|
|
|
347
|
-
|
|
522
|
+
// Event Graph only
|
|
523
|
+
import { next, apply, applyAll, getCandidateTasks } from 'yaml-flow/event-graph';
|
|
524
|
+
import { createInitialExecutionState, isExecutionComplete, detectStuckState } from 'yaml-flow/event-graph';
|
|
525
|
+
import { TASK_STATUS, COMPLETION_STRATEGIES, CONFLICT_STRATEGIES } from 'yaml-flow/event-graph';
|
|
348
526
|
|
|
349
|
-
|
|
527
|
+
// Stores
|
|
528
|
+
import { MemoryStore, LocalStorageStore, FileStore } from 'yaml-flow/stores';
|
|
350
529
|
|
|
351
|
-
|
|
530
|
+
// Backward compatibility (v1 names → v2)
|
|
531
|
+
import { FlowEngine, createEngine } from 'yaml-flow'; // aliases for StepMachine, createStepMachine
|
|
532
|
+
```
|
|
352
533
|
|
|
353
|
-
|
|
534
|
+
---
|
|
354
535
|
|
|
355
|
-
|
|
536
|
+
## API Reference
|
|
356
537
|
|
|
357
|
-
###
|
|
538
|
+
### Step Machine
|
|
539
|
+
|
|
540
|
+
| Export | Description |
|
|
541
|
+
|---|---|
|
|
542
|
+
| `createStepMachine(flow, handlers, options?)` | Create and validate a StepMachine instance |
|
|
543
|
+
| `StepMachine.run(initialData?)` | Execute flow from start, returns `StepMachineResult` |
|
|
544
|
+
| `StepMachine.pause(runId)` | Pause a running flow |
|
|
545
|
+
| `StepMachine.resume(runId)` | Resume a paused flow |
|
|
546
|
+
| `StepMachine.on(event, listener)` | Subscribe to events (`step:start`, `step:complete`, `flow:complete`, `transition`, etc.) |
|
|
547
|
+
| `loadStepFlow(path)` | Load + validate a YAML/JSON flow file |
|
|
548
|
+
| `applyStepResult(flow, state, step, result)` | Pure reducer: apply a step result to state |
|
|
549
|
+
| `checkCircuitBreaker(flow, state, step)` | Pure: check/increment circuit breaker |
|
|
550
|
+
| `computeStepInput(flow, step, allData)` | Pure: filter data to what a step expects |
|
|
551
|
+
| `createInitialState(flow, runId)` | Pure: create starting state |
|
|
552
|
+
|
|
553
|
+
### Event Graph
|
|
554
|
+
|
|
555
|
+
| Export | Description |
|
|
556
|
+
|---|---|
|
|
557
|
+
| `next(graph, state)` | Pure scheduler: returns `{ eligibleTasks, isComplete, stuckDetection, conflicts }` |
|
|
558
|
+
| `apply(state, event, graph)` | Pure reducer: apply one event, returns new state |
|
|
559
|
+
| `applyAll(state, events, graph)` | Apply multiple events sequentially |
|
|
560
|
+
| `createInitialExecutionState(graph, executionId)` | Create starting state for a graph |
|
|
561
|
+
| `getCandidateTasks(graph, state)` | Low-level: just the eligible task list |
|
|
562
|
+
| `isExecutionComplete(graph, state)` | Check completion against configured strategy |
|
|
563
|
+
| `detectStuckState({graph, state, ...})` | Check if execution is stuck |
|
|
564
|
+
| `addDynamicTask(graph, name, config)` | Immutably add a task to a graph config |
|
|
565
|
+
|
|
566
|
+
### Event Types (for `apply()`)
|
|
567
|
+
|
|
568
|
+
| Event | Fields | Effect |
|
|
569
|
+
|---|---|---|
|
|
570
|
+
| `task-started` | `taskName` | Sets task status to `running` |
|
|
571
|
+
| `task-completed` | `taskName`, `result?` | Marks completed, adds `provides` tokens (or `on[result]` tokens) |
|
|
572
|
+
| `task-failed` | `taskName`, `error` | Retries or marks failed, injects `on_failure` tokens |
|
|
573
|
+
| `task-progress` | `taskName`, `message?`, `progress?` | Updates progress/messages |
|
|
574
|
+
| `inject-tokens` | `tokens[]` | Adds tokens to available outputs (unblocks waiting tasks) |
|
|
575
|
+
| `agent-action` | `action: start\|stop\|pause\|resume` | Controls execution lifecycle |
|
|
576
|
+
| `task-creation` | `taskName`, `taskConfig` | Adds a new task to execution state |
|
|
577
|
+
|
|
578
|
+
---
|
|
358
579
|
|
|
359
|
-
|
|
580
|
+
## Examples
|
|
360
581
|
|
|
361
|
-
|
|
582
|
+
See the [examples/](./examples) directory:
|
|
362
583
|
|
|
363
|
-
|
|
584
|
+
| Example | Mode | Demonstrates |
|
|
585
|
+
|---|---|---|
|
|
586
|
+
| [Simple Greeting](./examples/node/simple-greeting.ts) | Step Machine | Basic flow with file store |
|
|
587
|
+
| [AI Conversation](./examples/node/ai-conversation.ts) | Step Machine | Retry, circuit breakers, component injection |
|
|
588
|
+
| [Research Pipeline](./examples/event-graph/research-pipeline.ts) | Event Graph | Parallel tasks, goal-based completion |
|
|
589
|
+
| [CI/CD Pipeline](./examples/event-graph/ci-cd-pipeline.ts) | Event Graph | External events, conditional routing, failure tokens |
|
|
590
|
+
| [Order Processing](./examples/flows/order-processing.yaml) | Step Machine | YAML flow definition |
|
|
591
|
+
| [Browser Demo](./examples/browser/index.html) | Step Machine | In-browser usage |
|
|
364
592
|
|
|
365
|
-
|
|
593
|
+
---
|
|
366
594
|
|
|
367
|
-
|
|
595
|
+
## Migrating from v1
|
|
368
596
|
|
|
369
|
-
|
|
597
|
+
v2 is backward compatible. The old names still work:
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
// v1 (still works)
|
|
601
|
+
import { FlowEngine, createEngine } from 'yaml-flow';
|
|
370
602
|
|
|
371
|
-
|
|
603
|
+
// v2 (preferred)
|
|
604
|
+
import { StepMachine, createStepMachine } from 'yaml-flow/step-machine';
|
|
605
|
+
```
|
|
372
606
|
|
|
373
|
-
|
|
374
|
-
- [AI Conversation (Node.js)](./examples/node/ai-conversation.ts)
|
|
375
|
-
- [Browser Demo](./examples/browser/index.html)
|
|
376
|
-
- [Order Processing (Flow)](./examples/flows/order-processing.yaml)
|
|
607
|
+
The `FlowStore` interface is now `StepMachineStore` (same shape). `RunState` is now `StepMachineState` (same shape). Both old names resolve to the new types.
|
|
377
608
|
|
|
378
609
|
## License
|
|
379
610
|
|