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.
Files changed (50) hide show
  1. package/README.md +486 -255
  2. package/dist/constants-D1fTEbbM.d.cts +330 -0
  3. package/dist/constants-D1fTEbbM.d.ts +330 -0
  4. package/dist/event-graph/index.cjs +895 -0
  5. package/dist/event-graph/index.cjs.map +1 -0
  6. package/dist/event-graph/index.d.cts +53 -0
  7. package/dist/event-graph/index.d.ts +53 -0
  8. package/dist/event-graph/index.js +855 -0
  9. package/dist/event-graph/index.js.map +1 -0
  10. package/dist/index.cjs +1128 -312
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +3 -2
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +1093 -306
  15. package/dist/index.js.map +1 -1
  16. package/dist/step-machine/index.cjs +513 -0
  17. package/dist/step-machine/index.cjs.map +1 -0
  18. package/dist/step-machine/index.d.cts +77 -0
  19. package/dist/step-machine/index.d.ts +77 -0
  20. package/dist/step-machine/index.js +502 -0
  21. package/dist/step-machine/index.js.map +1 -0
  22. package/dist/stores/file.cjs.map +1 -1
  23. package/dist/stores/file.d.cts +4 -4
  24. package/dist/stores/file.d.ts +4 -4
  25. package/dist/stores/file.js.map +1 -1
  26. package/dist/stores/index.cjs +232 -0
  27. package/dist/stores/index.cjs.map +1 -0
  28. package/dist/stores/index.d.cts +4 -0
  29. package/dist/stores/index.d.ts +4 -0
  30. package/dist/stores/index.js +228 -0
  31. package/dist/stores/index.js.map +1 -0
  32. package/dist/stores/localStorage.cjs.map +1 -1
  33. package/dist/stores/localStorage.d.cts +4 -4
  34. package/dist/stores/localStorage.d.ts +4 -4
  35. package/dist/stores/localStorage.js.map +1 -1
  36. package/dist/stores/memory.cjs.map +1 -1
  37. package/dist/stores/memory.d.cts +4 -4
  38. package/dist/stores/memory.d.ts +4 -4
  39. package/dist/stores/memory.js.map +1 -1
  40. package/dist/types-FZ_eyErS.d.cts +115 -0
  41. package/dist/types-FZ_eyErS.d.ts +115 -0
  42. package/package.json +16 -6
  43. package/dist/core/index.cjs +0 -557
  44. package/dist/core/index.cjs.map +0 -1
  45. package/dist/core/index.d.cts +0 -102
  46. package/dist/core/index.d.ts +0 -102
  47. package/dist/core/index.js +0 -549
  48. package/dist/core/index.js.map +0 -1
  49. package/dist/types-BoWndaAJ.d.cts +0 -237
  50. package/dist/types-BoWndaAJ.d.ts +0 -237
package/README.md CHANGED
@@ -1,379 +1,610 @@
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…
25
35
 
26
- ## Quick Start
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
- ### 1. Define Your Flow (YAML)
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
- # 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]
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
- success: done
42
- failure: error
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
88
- ```
199
+ completion: goal-reached
200
+ goal: [final-report]
201
+ conflict_strategy: parallel-all
89
202
 
90
- ### Steps
203
+ tasks:
204
+ fetch_sources:
205
+ provides: [raw-sources]
91
206
 
92
- ```yaml
93
- 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
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
- ### Terminal States
211
+ analyse_entities:
212
+ requires: [raw-sources]
213
+ provides: [entity-result]
113
214
 
114
- ```yaml
115
- terminal_states:
116
- success:
117
- return_intent: "success"
118
- return_artifacts:
119
- - result_data
120
- - metadata
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
- ## Step Handlers
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
- Step handlers are pure async functions:
226
+ ### 2. Write a driver loop
130
227
 
131
228
  ```typescript
132
- type StepHandler = (input: StepInput, context: StepContext) => Promise<StepResult>;
133
-
134
- interface StepInput {
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
- 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
- }
233
+ const graph = parse(readFileSync('./research-pipeline.yaml', 'utf8'));
234
+ let state = createInitialExecutionState(graph, 'exec-1');
146
235
 
147
- interface StepResult {
148
- result: string; // Transition key (e.g., 'success', 'failure')
149
- data?: object; // Output data (matches produces_data)
150
- }
151
- ```
236
+ while (true) {
237
+ const schedule = next(graph, state);
152
238
 
153
- ### Example Handler
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
- ```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
- };
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
- ## Storage Adapters
273
+ That's the entire integration. ~30 lines. The engine is pure; your loop owns the I/O.
184
274
 
185
- ### Memory (Default)
275
+ ### Event Graph features at a glance
186
276
 
187
- ```typescript
188
- import { MemoryStore } from 'yaml-flow';
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
- const engine = createEngine(flow, handlers, {
191
- store: new MemoryStore()
192
- });
193
- ```
291
+ ### Completion strategies explained
194
292
 
195
- ### LocalStorage (Browser)
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
- ```typescript
198
- import { LocalStorageStore } from 'yaml-flow/stores/localStorage';
301
+ ### Conflict resolution strategies
199
302
 
200
- const engine = createEngine(flow, handlers, {
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
- ### File System (Node.js)
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
- ```typescript
208
- import { FileStore } from 'yaml-flow/stores/file';
318
+ ---
209
319
 
210
- const engine = createEngine(flow, handlers, {
211
- store: new FileStore({ directory: './flow-data' })
212
- });
213
- ```
320
+ ## Practical Patterns
214
321
 
215
- ### Custom Store
322
+ ### Pattern: AI Agent Tool Orchestration (Event Graph)
216
323
 
217
- Implement the `FlowStore` interface:
324
+ An agent needs to gather evidence from multiple sources, then synthesize.
218
325
 
219
- ```typescript
220
- interface FlowStore {
221
- saveRunState(runId: string, state: RunState): Promise<void>;
222
- loadRunState(runId: string): Promise<RunState | null>;
223
- deleteRunState(runId: string): Promise<void>;
224
- setData(runId: string, key: string, value: unknown): Promise<void>;
225
- getData(runId: string, key: string): Promise<unknown>;
226
- getAllData(runId: string): Promise<Record<string, unknown>>;
227
- clearData(runId: string): Promise<void>;
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
- ## Component Injection
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
- Inject external dependencies (databases, API clients, etc.):
360
+ ### Pattern: Order Processing Pipeline (Step Machine)
234
361
 
235
- ```typescript
236
- const engine = createEngine(flow, handlers, {
237
- components: {
238
- db: databaseClient,
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
- // 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
- };
252
- ```
367
+ steps:
368
+ validate:
369
+ produces_data: [validated_order]
370
+ transitions:
371
+ valid: charge_payment
372
+ invalid: reject
253
373
 
254
- ## Events
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
- Subscribe to flow events:
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
- ```typescript
259
- const engine = createEngine(flow, handlers);
392
+ refund:
393
+ expects_data: [payment_id]
394
+ produces_data: [refund_id]
395
+ transitions:
396
+ done: reject
260
397
 
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
- });
398
+ confirm:
399
+ expects_data: [tracking_number]
400
+ transitions:
401
+ done: complete
265
402
 
266
- // Available events:
267
- // - flow:start, flow:complete, flow:error, flow:paused, flow:resumed
268
- // - step:start, step:complete, step:error
269
- // - transition
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
- ## Pause & Resume
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
- // Start flow
276
- const result = engine.run({ data: 'value' });
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
- // Pause (from another context)
279
- await engine.pause(runId);
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
- // Later: resume
282
- const resumed = await engine.resume(runId);
439
+ // Now next(graph, state) will return 'approve' as eligible
283
440
  ```
284
441
 
285
- ## Cancellation
442
+ ### Pattern: Conditional Branching in Event Graph
286
443
 
287
- ```typescript
288
- const controller = new AbortController();
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
- const engine = createEngine(flow, handlers, {
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
- // Start flow
295
- const resultPromise = engine.run();
468
+ ---
296
469
 
297
- // Cancel
298
- controller.abort();
299
- ```
470
+ ## Storage Adapters
300
471
 
301
- ## Browser Usage
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
- ### With Bundler (Vite, webpack, etc.)
474
+ ### Memory (default, all environments)
304
475
 
305
476
  ```typescript
306
- import { createEngine, MemoryStore } from 'yaml-flow';
477
+ import { MemoryStore } from 'yaml-flow/stores/memory';
478
+ ```
307
479
 
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
- };
480
+ ### LocalStorage (browser)
314
481
 
315
- const engine = createEngine(flow, handlers);
482
+ ```typescript
483
+ import { LocalStorageStore } from 'yaml-flow/stores/localStorage';
484
+ new LocalStorageStore({ prefix: 'myapp' });
316
485
  ```
317
486
 
318
- ### From URL
487
+ ### File System (Node.js)
319
488
 
320
489
  ```typescript
321
- import { loadFlowFromUrl, createEngine } from 'yaml-flow';
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
- ## JSON Schema
328
-
329
- Use the included JSON Schema for IDE autocomplete:
494
+ ### Custom Store
330
495
 
331
- ```yaml
332
- # yaml-language-server: $schema=node_modules/yaml-flow/schema/flow.schema.json
496
+ Implement the `StepMachineStore` interface:
333
497
 
334
- settings:
335
- start_step: my_step
336
- # ... IDE autocomplete works here
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
- ## API Reference
510
+ ---
340
511
 
341
- ### `createEngine(flow, handlers, options?)`
512
+ ## Package Exports
342
513
 
343
- Create a new flow engine instance.
514
+ ```typescript
515
+ // Everything (both modes + stores)
516
+ import { StepMachine, next, apply, MemoryStore } from 'yaml-flow';
344
517
 
345
- ### `loadFlow(source)`
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
- Load and validate a flow from file path, URL, or object.
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
- ### `FlowEngine.run(initialData?)`
527
+ // Stores
528
+ import { MemoryStore, LocalStorageStore, FileStore } from 'yaml-flow/stores';
350
529
 
351
- Execute the flow from start.
530
+ // Backward compatibility (v1 names → v2)
531
+ import { FlowEngine, createEngine } from 'yaml-flow'; // aliases for StepMachine, createStepMachine
532
+ ```
352
533
 
353
- ### `FlowEngine.resume(runId)`
534
+ ---
354
535
 
355
- Resume a paused flow.
536
+ ## API Reference
356
537
 
357
- ### `FlowEngine.pause(runId)`
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
- Pause a running flow.
580
+ ## Examples
360
581
 
361
- ### `FlowEngine.on(event, listener)`
582
+ See the [examples/](./examples) directory:
362
583
 
363
- Subscribe to flow events.
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
- ### `FlowEngine.getStore()`
593
+ ---
366
594
 
367
- Get the store instance.
595
+ ## Migrating from v1
368
596
 
369
- ## Examples
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
- See the [examples](./examples) directory:
603
+ // v2 (preferred)
604
+ import { StepMachine, createStepMachine } from 'yaml-flow/step-machine';
605
+ ```
372
606
 
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)
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