yaml-flow 1.0.0 → 2.1.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.
Files changed (62) hide show
  1. package/README.md +705 -225
  2. package/dist/batch/index.cjs +109 -0
  3. package/dist/batch/index.cjs.map +1 -0
  4. package/dist/batch/index.d.cts +126 -0
  5. package/dist/batch/index.d.ts +126 -0
  6. package/dist/batch/index.js +107 -0
  7. package/dist/batch/index.js.map +1 -0
  8. package/dist/config/index.cjs +80 -0
  9. package/dist/config/index.cjs.map +1 -0
  10. package/dist/config/index.d.cts +71 -0
  11. package/dist/config/index.d.ts +71 -0
  12. package/dist/config/index.js +77 -0
  13. package/dist/config/index.js.map +1 -0
  14. package/dist/constants-D1fTEbbM.d.cts +330 -0
  15. package/dist/constants-D1fTEbbM.d.ts +330 -0
  16. package/dist/event-graph/index.cjs +895 -0
  17. package/dist/event-graph/index.cjs.map +1 -0
  18. package/dist/event-graph/index.d.cts +53 -0
  19. package/dist/event-graph/index.d.ts +53 -0
  20. package/dist/event-graph/index.js +855 -0
  21. package/dist/event-graph/index.js.map +1 -0
  22. package/dist/index.cjs +1309 -312
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +5 -2
  25. package/dist/index.d.ts +5 -2
  26. package/dist/index.js +1271 -306
  27. package/dist/index.js.map +1 -1
  28. package/dist/step-machine/index.cjs +513 -0
  29. package/dist/step-machine/index.cjs.map +1 -0
  30. package/dist/step-machine/index.d.cts +77 -0
  31. package/dist/step-machine/index.d.ts +77 -0
  32. package/dist/step-machine/index.js +502 -0
  33. package/dist/step-machine/index.js.map +1 -0
  34. package/dist/stores/file.cjs.map +1 -1
  35. package/dist/stores/file.d.cts +4 -4
  36. package/dist/stores/file.d.ts +4 -4
  37. package/dist/stores/file.js.map +1 -1
  38. package/dist/stores/index.cjs +232 -0
  39. package/dist/stores/index.cjs.map +1 -0
  40. package/dist/stores/index.d.cts +4 -0
  41. package/dist/stores/index.d.ts +4 -0
  42. package/dist/stores/index.js +228 -0
  43. package/dist/stores/index.js.map +1 -0
  44. package/dist/stores/localStorage.cjs.map +1 -1
  45. package/dist/stores/localStorage.d.cts +4 -4
  46. package/dist/stores/localStorage.d.ts +4 -4
  47. package/dist/stores/localStorage.js.map +1 -1
  48. package/dist/stores/memory.cjs.map +1 -1
  49. package/dist/stores/memory.d.cts +4 -4
  50. package/dist/stores/memory.d.ts +4 -4
  51. package/dist/stores/memory.js.map +1 -1
  52. package/dist/types-FZ_eyErS.d.cts +115 -0
  53. package/dist/types-FZ_eyErS.d.ts +115 -0
  54. package/package.json +26 -6
  55. package/dist/core/index.cjs +0 -557
  56. package/dist/core/index.cjs.map +0 -1
  57. package/dist/core/index.d.cts +0 -102
  58. package/dist/core/index.d.ts +0 -102
  59. package/dist/core/index.js +0 -549
  60. package/dist/core/index.js.map +0 -1
  61. package/dist/types-BoWndaAJ.d.cts +0 -237
  62. package/dist/types-BoWndaAJ.d.ts +0 -237
package/README.md CHANGED
@@ -1,225 +1,504 @@
1
1
  # yaml-flow
2
2
 
3
- A lightweight, isomorphic workflow engine with declarative YAML flows and pluggable persistence.
3
+ Two workflow engines in one package. Pick the model that fits your problem.
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/yaml-flow.svg)](https://www.npmjs.com/package/yaml-flow)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- ## Features
8
+ ```
9
+ npm install yaml-flow
10
+ ```
9
11
 
10
- - **Isomorphic** Runs in both browser and Node.js
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
- ## Installation
14
+ yaml-flow ships two execution models. They solve fundamentally different problems.
21
15
 
22
- ```bash
23
- npm install yaml-flow
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…
35
+
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.
42
+
43
+ **Examples:** CI/CD pipelines, agent tool orchestration, research workflows (fetch → analyse A | analyse B → merge), build systems, multi-model AI routing, eligibility engines.
25
44
 
26
- ## Quick Start
45
+ ### When in doubt
27
46
 
28
- ### 1. Define Your Flow (YAML)
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
- # my-flow.yaml
59
+ # support-ticket.yaml
32
60
  settings:
33
- start_step: greet
34
- max_total_steps: 10
61
+ start_step: classify
62
+ max_total_steps: 20
35
63
 
36
64
  steps:
37
- greet:
38
- produces_data:
39
- - message
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]
40
75
  transitions:
41
- success: done
42
- failure: error
76
+ resolved: close_ticket
77
+ escalate: escalate_ticket
78
+
79
+ handle_technical:
80
+ expects_data: [category]
81
+ produces_data: [resolution]
82
+ transitions:
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
- done:
46
- return_intent: success
47
- return_artifacts: message
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. Create Step Handlers
107
+ ### 2. Write handlers and run
55
108
 
56
109
  ```typescript
57
- import { createEngine, loadFlow, MemoryStore } from 'yaml-flow';
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
- greet: async (input, ctx) => {
61
- return {
62
- result: 'success',
63
- data: { message: `Hello, ${input.name}!` }
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
- ### 3. Run the Flow
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
- const flow = await loadFlow('./my-flow.yaml');
73
- const engine = createEngine(flow, handlers);
165
+ import { createInitialState, applyStepResult, checkCircuitBreaker, computeStepInput } from 'yaml-flow/step-machine';
166
+
167
+ let state = createInitialState(flow, 'run-1');
74
168
 
75
- const result = await engine.run({ name: 'World' });
76
- console.log(result.data.message); // "Hello, World!"
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
- ## Flow Configuration
183
+ ---
80
184
 
81
- ### Settings
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
- start_step: my_step # Required: First step to execute
86
- max_total_steps: 100 # Optional: Circuit breaker (default: 100)
87
- timeout_ms: 60000 # Optional: Flow timeout in ms
199
+ completion: goal-reached
200
+ goal: [final-report]
201
+ conflict_strategy: parallel-all
202
+
203
+ tasks:
204
+ fetch_sources:
205
+ provides: [raw-sources]
206
+
207
+ analyse_sentiment:
208
+ requires: [raw-sources]
209
+ provides: [sentiment-result]
210
+
211
+ analyse_entities:
212
+ requires: [raw-sources]
213
+ provides: [entity-result]
214
+
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]
222
+ ```
223
+
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.
225
+
226
+ ### 2. Write a driver loop
227
+
228
+ ```typescript
229
+ import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
230
+ import { parse } from 'yaml';
231
+ import { readFileSync } from 'fs';
232
+
233
+ const graph = parse(readFileSync('./research-pipeline.yaml', 'utf8'));
234
+ let state = createInitialExecutionState(graph, 'exec-1');
235
+
236
+ while (true) {
237
+ const schedule = next(graph, state);
238
+
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
+ }
247
+
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);
268
+ }
269
+ }
270
+ }
271
+ ```
272
+
273
+ That's the entire integration. ~30 lines. The engine is pure; your loop owns the I/O.
274
+
275
+ ### Event Graph features at a glance
276
+
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 |
290
+
291
+ ### Completion strategies explained
292
+
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 |
300
+
301
+ ### Conflict resolution strategies
302
+
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:
304
+
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 |
317
+
318
+ ---
319
+
320
+ ## Practical Patterns
321
+
322
+ ### Pattern: AI Agent Tool Orchestration (Event Graph)
323
+
324
+ An agent needs to gather evidence from multiple sources, then synthesize.
325
+
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 }
88
356
  ```
89
357
 
90
- ### Steps
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.
359
+
360
+ ### Pattern: Order Processing Pipeline (Step Machine)
91
361
 
92
362
  ```yaml
363
+ settings:
364
+ start_step: validate
365
+ max_total_steps: 15
366
+
93
367
  steps:
94
- my_step:
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
368
+ validate:
369
+ produces_data: [validated_order]
370
+ transitions:
371
+ valid: charge_payment
372
+ invalid: reject
373
+
374
+ charge_payment:
375
+ expects_data: [validated_order]
376
+ produces_data: [payment_id]
377
+ transitions:
378
+ success: ship
379
+ declined: reject
380
+ retry:
104
381
  max_attempts: 3
105
- delay_ms: 1000
382
+ delay_ms: 2000
106
383
  backoff_multiplier: 2
107
- circuit_breaker: # Optional loop protection
108
- max_iterations: 5
109
- on_open: fallback_step
110
- ```
111
384
 
112
- ### Terminal States
385
+ ship:
386
+ expects_data: [validated_order, payment_id]
387
+ produces_data: [tracking_number]
388
+ transitions:
389
+ shipped: confirm
390
+ failure: refund
391
+
392
+ refund:
393
+ expects_data: [payment_id]
394
+ produces_data: [refund_id]
395
+ transitions:
396
+ done: reject
397
+
398
+ confirm:
399
+ expects_data: [tracking_number]
400
+ transitions:
401
+ done: complete
113
402
 
114
- ```yaml
115
403
  terminal_states:
116
- success:
117
- return_intent: "success"
118
- return_artifacts:
119
- - result_data
120
- - metadata
121
-
122
- error:
123
- return_intent: "error"
404
+ complete:
405
+ return_intent: success
406
+ return_artifacts: [tracking_number, payment_id]
407
+ reject:
408
+ return_intent: rejected
124
409
  return_artifacts: false
125
410
  ```
126
411
 
127
- ## Step Handlers
412
+ Linear with branches. The current step decides what's next. Retry on payment failures with exponential backoff.
128
413
 
129
- Step handlers are pure async functions:
414
+ ### Pattern: Inject External Events (Event Graph)
130
415
 
131
416
  ```typescript
132
- type StepHandler = (input: StepInput, context: StepContext) => Promise<StepResult>;
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
+ };
133
427
 
134
- interface StepInput {
135
- [key: string]: unknown; // Data from expects_data
136
- }
428
+ let state = createInitialExecutionState(graph, 'deploy-1');
137
429
 
138
- interface StepContext {
139
- runId: string; // Current run ID
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
- }
430
+ // ... run build and test normally ...
146
431
 
147
- interface StepResult {
148
- result: string; // Transition key (e.g., 'success', 'failure')
149
- data?: object; // Output data (matches produces_data)
150
- }
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);
438
+
439
+ // Now next(graph, state) will return 'approve' as eligible
151
440
  ```
152
441
 
153
- ### Example Handler
442
+ ### Pattern: Conditional Branching in Event Graph
154
443
 
155
- ```typescript
156
- const handlers = {
157
- async processOrder(input, ctx) {
158
- const { order } = input;
159
-
160
- // Access injected database client
161
- const db = ctx.components.db;
162
-
163
- try {
164
- const savedOrder = await db.orders.save(order);
165
-
166
- return {
167
- result: 'success',
168
- data: {
169
- order_id: savedOrder.id,
170
- status: 'pending'
171
- }
172
- };
173
- } catch (error) {
174
- return {
175
- result: 'failure',
176
- data: { error: error.message }
177
- };
178
- }
179
- }
180
- };
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]
181
464
  ```
182
465
 
466
+ Only one downstream path activates based on the classifier result. This is the event-graph equivalent of a switch statement.
467
+
468
+ ---
469
+
183
470
  ## Storage Adapters
184
471
 
185
- ### Memory (Default)
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.
186
473
 
187
- ```typescript
188
- import { MemoryStore } from 'yaml-flow';
474
+ ### Memory (default, all environments)
189
475
 
190
- const engine = createEngine(flow, handlers, {
191
- store: new MemoryStore()
192
- });
476
+ ```typescript
477
+ import { MemoryStore } from 'yaml-flow/stores/memory';
193
478
  ```
194
479
 
195
- ### LocalStorage (Browser)
480
+ ### LocalStorage (browser)
196
481
 
197
482
  ```typescript
198
483
  import { LocalStorageStore } from 'yaml-flow/stores/localStorage';
199
-
200
- const engine = createEngine(flow, handlers, {
201
- store: new LocalStorageStore({ prefix: 'myapp' })
202
- });
484
+ new LocalStorageStore({ prefix: 'myapp' });
203
485
  ```
204
486
 
205
487
  ### File System (Node.js)
206
488
 
207
489
  ```typescript
208
490
  import { FileStore } from 'yaml-flow/stores/file';
209
-
210
- const engine = createEngine(flow, handlers, {
211
- store: new FileStore({ directory: './flow-data' })
212
- });
491
+ new FileStore({ directory: './flow-data' });
213
492
  ```
214
493
 
215
494
  ### Custom Store
216
495
 
217
- Implement the `FlowStore` interface:
496
+ Implement the `StepMachineStore` interface:
218
497
 
219
498
  ```typescript
220
- interface FlowStore {
221
- saveRunState(runId: string, state: RunState): Promise<void>;
222
- loadRunState(runId: string): Promise<RunState | null>;
499
+ interface StepMachineStore {
500
+ saveRunState(runId: string, state: StepMachineState): Promise<void>;
501
+ loadRunState(runId: string): Promise<StepMachineState | null>;
223
502
  deleteRunState(runId: string): Promise<void>;
224
503
  setData(runId: string, key: string, value: unknown): Promise<void>;
225
504
  getData(runId: string, key: string): Promise<unknown>;
@@ -228,152 +507,353 @@ interface FlowStore {
228
507
  }
229
508
  ```
230
509
 
231
- ## Component Injection
510
+ ---
232
511
 
233
- Inject external dependencies (databases, API clients, etc.):
512
+ ## Batch Processing
513
+
514
+ yaml-flow includes a `batch()` utility for running multiple items through a flow concurrently. It works with both Step Machine and Event Graph — you provide the processor, it manages concurrency.
515
+
516
+ ### Quick Start
234
517
 
235
518
  ```typescript
236
- const engine = createEngine(flow, handlers, {
237
- components: {
238
- db: databaseClient,
239
- api: httpClient,
240
- cache: redisClient,
241
- ai: openAIClient
242
- }
519
+ import { batch } from 'yaml-flow/batch';
520
+ import { createStepMachine } from 'yaml-flow/step-machine';
521
+
522
+ const tickets = [
523
+ { id: 'T-001', message: 'Billing error' },
524
+ { id: 'T-002', message: 'App crashes on login' },
525
+ { id: 'T-003', message: 'Password reset help' },
526
+ ];
527
+
528
+ const result = await batch(tickets, {
529
+ concurrency: 3,
530
+ processor: async (ticket) => {
531
+ const machine = createStepMachine(flow, handlers);
532
+ return machine.run({ message: ticket.message });
533
+ },
534
+ onProgress: (p) => console.log(`${p.percent}% done`),
243
535
  });
244
536
 
245
- // Access in handlers
246
- const handlers = {
247
- async fetchData(input, ctx) {
248
- const result = await ctx.components.api.get('/data');
249
- return { result: 'success', data: { fetched: result } };
250
- }
251
- };
537
+ console.log(`${result.completed} succeeded, ${result.failed} failed`);
252
538
  ```
253
539
 
254
- ## Events
540
+ ### Options
541
+
542
+ | Option | Type | Default | Description |
543
+ |---|---|---|---|
544
+ | `concurrency` | `number` | `5` | Max parallel processors |
545
+ | `processor` | `(item, index) => Promise<TResult>` | *required* | Async function to process each item |
546
+ | `signal` | `AbortSignal` | — | Cancel remaining items |
547
+ | `onItemComplete` | `(item, result, index) => void` | — | Called when an item succeeds |
548
+ | `onItemError` | `(item, error, index) => void` | — | Called when an item fails |
549
+ | `onProgress` | `(progress) => void` | — | Called after each item with `{ completed, failed, active, pending, total, percent, elapsedMs }` |
255
550
 
256
- Subscribe to flow events:
551
+ ### Result Shape
257
552
 
258
553
  ```typescript
259
- const engine = createEngine(flow, handlers);
554
+ {
555
+ items: BatchItemResult[]; // Per-item: { item, index, status, result?, error?, durationMs }
556
+ completed: number; // Items that succeeded
557
+ failed: number; // Items that threw
558
+ total: number;
559
+ durationMs: number; // Wall-clock time for entire batch
560
+ }
561
+ ```
260
562
 
261
- // Subscribe to events
262
- const unsubscribe = engine.on('step:complete', (event) => {
263
- console.log(`Step ${event.data.step} completed with ${event.data.result}`);
264
- });
563
+ ### Works with Event Graph too
265
564
 
266
- // Available events:
267
- // - flow:start, flow:complete, flow:error, flow:paused, flow:resumed
268
- // - step:start, step:complete, step:error
269
- // - transition
565
+ ```typescript
566
+ import { batch } from 'yaml-flow/batch';
567
+ import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
568
+
569
+ const result = await batch(items, {
570
+ concurrency: 5,
571
+ processor: async (item) => {
572
+ let state = createInitialExecutionState(graph, `run-${item.id}`);
573
+ state = apply(state, { type: 'inject-tokens', tokens: [item.readyToken], timestamp: Date.now() }, graph);
574
+ // ... drive graph loop with next() + apply()
575
+ return state;
576
+ },
577
+ });
270
578
  ```
271
579
 
272
- ## Pause & Resume
580
+ ---
273
581
 
274
- ```typescript
275
- // Start flow
276
- const result = engine.run({ data: 'value' });
582
+ ## Config Utilities
277
583
 
278
- // Pause (from another context)
279
- await engine.pause(runId);
584
+ Pure pre-processing transforms you apply before passing config to the engine. They never touch engine state — just config in, config out.
280
585
 
281
- // Later: resume
282
- const resumed = await engine.resume(runId);
283
- ```
586
+ ### Variable Interpolation
284
587
 
285
- ## Cancellation
588
+ Replace `${KEY}` patterns in any config object. Works with both GraphConfig and StepFlowConfig.
286
589
 
287
590
  ```typescript
288
- const controller = new AbortController();
591
+ import { resolveVariables } from 'yaml-flow/config';
289
592
 
290
- const engine = createEngine(flow, handlers, {
291
- signal: controller.signal
593
+ const resolved = resolveVariables(graphConfig, {
594
+ ENTITY_ID: 'ticket-42',
595
+ TOOLS_DIR: '/opt/tools',
596
+ WORKDIR: '/data/workdata',
292
597
  });
598
+ // Every ${ENTITY_ID} in task configs, cmd-args, etc. → replaced
599
+ ```
293
600
 
294
- // Start flow
295
- const resultPromise = engine.run();
601
+ ### Config Templates
296
602
 
297
- // Cancel
298
- controller.abort();
603
+ DRY reusable config blocks. Tasks reference a named template via `config-template`; the function deep-merges template + task overrides and removes the reference.
604
+
605
+ ```typescript
606
+ import { resolveConfigTemplates } from 'yaml-flow/config';
607
+
608
+ const config = {
609
+ configTemplates: { // or 'config-templates' (kebab-case)
610
+ PYTHON_TOOL: { cmd: 'python', timeout: 30000, cwd: '/workdata' },
611
+ NODE_CMD: { cmd: 'node', timeout: 60000 },
612
+ },
613
+ tasks: {
614
+ analyze: { provides: ['analysis'], config: { 'config-template': 'PYTHON_TOOL', 'cmd-args': 'analyze.py' } },
615
+ build: { provides: ['build'], config: { 'config-template': 'NODE_CMD', script: 'build.js' } },
616
+ },
617
+ };
618
+
619
+ const resolved = resolveConfigTemplates(config);
620
+ // analyze.config → { cmd: 'python', timeout: 30000, cwd: '/workdata', 'cmd-args': 'analyze.py' }
621
+ // configTemplates key removed from output
299
622
  ```
300
623
 
301
- ## Browser Usage
624
+ ### Composing Both
302
625
 
303
- ### With Bundler (Vite, webpack, etc.)
626
+ Templates first (expands references), then variables (fills in `${...}` placeholders):
304
627
 
305
628
  ```typescript
306
- import { createEngine, MemoryStore } from 'yaml-flow';
629
+ import { resolveConfigTemplates, resolveVariables } from 'yaml-flow/config';
307
630
 
308
- // Flow as JSON (pre-parsed at build time)
309
- const flow = {
310
- settings: { start_step: 'start' },
311
- steps: { start: { transitions: { success: 'done' } } },
312
- terminal_states: { done: { return_intent: 'success' } }
313
- };
631
+ const raw = loadYaml('pipeline.yaml'); // has configTemplates + ${VAR} refs
632
+ const resolved = resolveVariables(
633
+ resolveConfigTemplates(raw),
634
+ { ENTITY_ID: 'url-42', TOOLS_DIR: '/opt/tools' },
635
+ );
636
+ ```
637
+
638
+ ---
639
+
640
+ ## Graph-of-Graphs Pattern
641
+
642
+ Real-world pipelines are often **layered**: an outer orchestration graph where some tasks are themselves entire sub-workflows — each processing a batch of items through their own DAG or step flow. yaml-flow doesn't bake this into the engine (the pure scheduler stays simple), but the primitives compose cleanly.
643
+
644
+ ### The Shape
314
645
 
315
- const engine = createEngine(flow, handlers);
316
646
  ```
647
+ Outer graph (event-graph)
648
+ ├── prep-workdata → plain task
649
+ ├── copy-input-files → plain task
650
+ ├── evidence-gathering → batch × inner event-graph (N items, 5 concurrent)
651
+ ├── grade-synthesis → batch × inner step-machine (N items, 3 concurrent)
652
+ ├── analyze-mismatches → plain task
653
+ ├── [health-check ∥ report]→ parallel plain tasks
654
+ └── archive-results → waits for both
655
+ ```
656
+
657
+ The outer graph sequences coarse stages. Some stages fan out into batches where each item runs through its own sub-workflow. The sub can be either mode.
658
+
659
+ ### How to Wire It
317
660
 
318
- ### From URL
661
+ Each "sub-graph task" in the outer graph is just a handler that composes `resolveConfigTemplates` → `resolveVariables` → `batch` → engine:
319
662
 
320
663
  ```typescript
321
- import { loadFlowFromUrl, createEngine } from 'yaml-flow';
664
+ import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
665
+ import { createStepMachine } from 'yaml-flow/step-machine';
666
+ import { batch } from 'yaml-flow/batch';
667
+ import { resolveVariables, resolveConfigTemplates } from 'yaml-flow/config';
668
+
669
+ // Outer graph handler for a sub-graph task (event-graph sub)
670
+ async function runEvidenceBatch(items, rawSubConfig) {
671
+ return batch(items, {
672
+ concurrency: 5,
673
+ processor: async (item) => {
674
+ // Resolve config per-item (each item gets its own ENTITY_ID)
675
+ const config = resolveVariables(
676
+ resolveConfigTemplates(rawSubConfig),
677
+ { ENTITY_ID: item.id, TOOLS_DIR: '/opt/tools' },
678
+ );
679
+ // Drive the inner event-graph
680
+ let state = createInitialExecutionState(config, `run-${item.id}`);
681
+ while (true) {
682
+ const { eligibleTasks, isComplete } = next(config, state);
683
+ if (isComplete) break;
684
+ for (const task of eligibleTasks) {
685
+ state = apply(state, { type: 'task-started', taskName: task, timestamp: new Date().toISOString() }, config);
686
+ const result = await executeTask(task, config.tasks[task], item);
687
+ state = apply(state, { type: 'task-completed', taskName: task, result, timestamp: new Date().toISOString() }, config);
688
+ }
689
+ }
690
+ return state;
691
+ },
692
+ });
693
+ }
322
694
 
323
- const flow = await loadFlowFromUrl('/flows/my-flow.json');
324
- const engine = createEngine(flow, handlers);
695
+ // Outer graph handler for a sub-graph task (step-machine sub)
696
+ async function runGradeBatch(items, flowConfig, handlers) {
697
+ return batch(items, {
698
+ concurrency: 3,
699
+ processor: async (item) => {
700
+ const machine = createStepMachine(flowConfig, handlers);
701
+ return machine.run({ entityId: item.id, evidence: item.evidence });
702
+ },
703
+ });
704
+ }
325
705
  ```
326
706
 
327
- ## JSON Schema
707
+ ### Driving the Outer Graph
328
708
 
329
- Use the included JSON Schema for IDE autocomplete:
709
+ The outer graph itself is an event-graph. Each handler maps to a task:
330
710
 
331
- ```yaml
332
- # yaml-language-server: $schema=node_modules/yaml-flow/schema/flow.schema.json
711
+ ```typescript
712
+ const outerHandlers = {
713
+ 'prep-workdata': async () => { /* setup */ },
714
+ 'copy-input-files': async () => { /* parse CSV, return items */ },
715
+ 'evidence-batch': async (ctx) => runEvidenceBatch(ctx.items, evidenceConfig),
716
+ 'grade-batch': async (ctx) => runGradeBatch(ctx.items, gradeFlow, gradeHandlers),
717
+ 'analyze-mismatches':async (ctx) => { /* compare grades */ },
718
+ 'health-check': async (ctx) => { /* validate */ },
719
+ 'generate-report': async (ctx) => { /* summarize */ },
720
+ 'archive': async (ctx) => { /* move outputs */ },
721
+ };
333
722
 
334
- settings:
335
- start_step: my_step
336
- # ... IDE autocomplete works here
723
+ // Simple outer loop
724
+ let state = createInitialExecutionState(outerGraph, 'pipeline-run-1');
725
+ while (true) {
726
+ const { eligibleTasks, isComplete } = next(outerGraph, state);
727
+ if (isComplete) break;
728
+ await Promise.all(eligibleTasks.map(async (taskName) => {
729
+ state = apply(state, { type: 'task-started', taskName, timestamp: now() }, outerGraph);
730
+ try {
731
+ await outerHandlers[taskName](context);
732
+ state = apply(state, { type: 'task-completed', taskName, timestamp: now() }, outerGraph);
733
+ } catch (err) {
734
+ state = apply(state, { type: 'task-failed', taskName, error: err.message, timestamp: now() }, outerGraph);
735
+ }
736
+ }));
737
+ }
337
738
  ```
338
739
 
339
- ## API Reference
740
+ ### Why Not Bake It Into the Engine?
340
741
 
341
- ### `createEngine(flow, handlers, options?)`
742
+ - The pure scheduler (`next`/`apply`) stays a simple `f(state, event) → newState`.
743
+ - Sub-graph execution involves file I/O, process spawning, HTTP calls — all driver concerns.
744
+ - Every deployment customizes how sub-tasks execute: in-process, `execSync`, HTTP, serverless.
745
+ - The primitives (`batch` + `resolveVariables` + `resolveConfigTemplates` + both engines) compose without coupling.
342
746
 
343
- Create a new flow engine instance.
747
+ See the [examples/graph-of-graphs/](./examples/graph-of-graphs/) directory for complete runnable examples.
344
748
 
345
- ### `loadFlow(source)`
749
+ ---
346
750
 
347
- Load and validate a flow from file path, URL, or object.
751
+ ## Package Exports
348
752
 
349
- ### `FlowEngine.run(initialData?)`
753
+ ```typescript
754
+ // Everything (both modes + stores + batch)
755
+ import { StepMachine, next, apply, MemoryStore, batch } from 'yaml-flow';
350
756
 
351
- Execute the flow from start.
757
+ // Step Machine only
758
+ import { StepMachine, createStepMachine, loadStepFlow } from 'yaml-flow/step-machine';
759
+ import { applyStepResult, checkCircuitBreaker, createInitialState } from 'yaml-flow/step-machine';
352
760
 
353
- ### `FlowEngine.resume(runId)`
761
+ // Event Graph only
762
+ import { next, apply, applyAll, getCandidateTasks } from 'yaml-flow/event-graph';
763
+ import { createInitialExecutionState, isExecutionComplete, detectStuckState } from 'yaml-flow/event-graph';
764
+ import { TASK_STATUS, COMPLETION_STRATEGIES, CONFLICT_STRATEGIES } from 'yaml-flow/event-graph';
354
765
 
355
- Resume a paused flow.
766
+ // Stores
767
+ import { MemoryStore, LocalStorageStore, FileStore } from 'yaml-flow/stores';
356
768
 
357
- ### `FlowEngine.pause(runId)`
769
+ // Batch
770
+ import { batch } from 'yaml-flow/batch';
771
+ import type { BatchOptions, BatchResult, BatchItemResult, BatchProgress } from 'yaml-flow/batch';
358
772
 
359
- Pause a running flow.
773
+ // Config utilities
774
+ import { resolveVariables, resolveConfigTemplates } from 'yaml-flow/config';
360
775
 
361
- ### `FlowEngine.on(event, listener)`
776
+ // Backward compatibility (v1 names → v2)
777
+ import { FlowEngine, createEngine } from 'yaml-flow'; // aliases for StepMachine, createStepMachine
778
+ ```
362
779
 
363
- Subscribe to flow events.
780
+ ---
364
781
 
365
- ### `FlowEngine.getStore()`
782
+ ## API Reference
366
783
 
367
- Get the store instance.
784
+ ### Step Machine
785
+
786
+ | Export | Description |
787
+ |---|---|
788
+ | `createStepMachine(flow, handlers, options?)` | Create and validate a StepMachine instance |
789
+ | `StepMachine.run(initialData?)` | Execute flow from start, returns `StepMachineResult` |
790
+ | `StepMachine.pause(runId)` | Pause a running flow |
791
+ | `StepMachine.resume(runId)` | Resume a paused flow |
792
+ | `StepMachine.on(event, listener)` | Subscribe to events (`step:start`, `step:complete`, `flow:complete`, `transition`, etc.) |
793
+ | `loadStepFlow(path)` | Load + validate a YAML/JSON flow file |
794
+ | `applyStepResult(flow, state, step, result)` | Pure reducer: apply a step result to state |
795
+ | `checkCircuitBreaker(flow, state, step)` | Pure: check/increment circuit breaker |
796
+ | `computeStepInput(flow, step, allData)` | Pure: filter data to what a step expects |
797
+ | `createInitialState(flow, runId)` | Pure: create starting state |
798
+
799
+ ### Event Graph
800
+
801
+ | Export | Description |
802
+ |---|---|
803
+ | `next(graph, state)` | Pure scheduler: returns `{ eligibleTasks, isComplete, stuckDetection, conflicts }` |
804
+ | `apply(state, event, graph)` | Pure reducer: apply one event, returns new state |
805
+ | `applyAll(state, events, graph)` | Apply multiple events sequentially |
806
+ | `createInitialExecutionState(graph, executionId)` | Create starting state for a graph |
807
+ | `getCandidateTasks(graph, state)` | Low-level: just the eligible task list |
808
+ | `isExecutionComplete(graph, state)` | Check completion against configured strategy |
809
+ | `detectStuckState({graph, state, ...})` | Check if execution is stuck |
810
+ | `addDynamicTask(graph, name, config)` | Immutably add a task to a graph config |
811
+
812
+ ### Event Types (for `apply()`)
813
+
814
+ | Event | Fields | Effect |
815
+ |---|---|---|
816
+ | `task-started` | `taskName` | Sets task status to `running` |
817
+ | `task-completed` | `taskName`, `result?` | Marks completed, adds `provides` tokens (or `on[result]` tokens) |
818
+ | `task-failed` | `taskName`, `error` | Retries or marks failed, injects `on_failure` tokens |
819
+ | `task-progress` | `taskName`, `message?`, `progress?` | Updates progress/messages |
820
+ | `inject-tokens` | `tokens[]` | Adds tokens to available outputs (unblocks waiting tasks) |
821
+ | `agent-action` | `action: start\|stop\|pause\|resume` | Controls execution lifecycle |
822
+ | `task-creation` | `taskName`, `taskConfig` | Adds a new task to execution state |
823
+
824
+ ---
368
825
 
369
826
  ## Examples
370
827
 
371
- See the [examples](./examples) directory:
828
+ See the [examples/](./examples) directory:
829
+
830
+ | Example | Mode | Demonstrates |
831
+ |---|---|---|
832
+ | [Simple Greeting](./examples/node/simple-greeting.ts) | Step Machine | Basic flow with file store |
833
+ | [AI Conversation](./examples/node/ai-conversation.ts) | Step Machine | Retry, circuit breakers, component injection |
834
+ | [Research Pipeline](./examples/event-graph/research-pipeline.ts) | Event Graph | Parallel tasks, goal-based completion |
835
+ | [CI/CD Pipeline](./examples/event-graph/ci-cd-pipeline.ts) | Event Graph | External events, conditional routing, failure tokens |
836
+ | [Batch Tickets](./examples/batch/batch-step-machine.ts) | Batch | Concurrent processing, progress tracking |
837
+ | [URL Pipeline](./examples/graph-of-graphs/url-processing-pipeline.ts) | Graph-of-Graphs | Outer event-graph → batch × inner event-graph per item |
838
+ | [Multi-Stage ETL](./examples/graph-of-graphs/multi-stage-etl.ts) | Graph-of-Graphs | Mixed modes: event-graph outer → step-machine + event-graph subs |
839
+ | [Order Processing](./examples/flows/order-processing.yaml) | Step Machine | YAML flow definition |
840
+ | [Browser Demo](./examples/browser/index.html) | Step Machine | In-browser usage |
841
+
842
+ ---
843
+
844
+ ## Migrating from v1
845
+
846
+ v2 is backward compatible. The old names still work:
847
+
848
+ ```typescript
849
+ // v1 (still works)
850
+ import { FlowEngine, createEngine } from 'yaml-flow';
851
+
852
+ // v2 (preferred)
853
+ import { StepMachine, createStepMachine } from 'yaml-flow/step-machine';
854
+ ```
372
855
 
373
- - [Simple Greeting (Node.js)](./examples/node/simple-greeting.ts)
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)
856
+ The `FlowStore` interface is now `StepMachineStore` (same shape). `RunState` is now `StepMachineState` (same shape). Both old names resolve to the new types.
377
857
 
378
858
  ## License
379
859