@cogitator-ai/workflows 0.5.17 → 0.7.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 (59) hide show
  1. package/README.md +339 -772
  2. package/dist/builder.d.ts +5 -4
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +59 -76
  5. package/dist/builder.js.map +1 -1
  6. package/dist/checkpoint.d.ts.map +1 -1
  7. package/dist/checkpoint.js +7 -3
  8. package/dist/checkpoint.js.map +1 -1
  9. package/dist/executor.d.ts +31 -2
  10. package/dist/executor.d.ts.map +1 -1
  11. package/dist/executor.js +217 -36
  12. package/dist/executor.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/manager/workflow-manager.d.ts +10 -0
  18. package/dist/manager/workflow-manager.d.ts.map +1 -1
  19. package/dist/manager/workflow-manager.js +32 -1
  20. package/dist/manager/workflow-manager.js.map +1 -1
  21. package/dist/nodes/adapters.d.ts +60 -0
  22. package/dist/nodes/adapters.d.ts.map +1 -0
  23. package/dist/nodes/adapters.js +138 -0
  24. package/dist/nodes/adapters.js.map +1 -0
  25. package/dist/nodes/agent.d.ts.map +1 -1
  26. package/dist/nodes/agent.js +2 -1
  27. package/dist/nodes/agent.js.map +1 -1
  28. package/dist/nodes/base.d.ts +9 -1
  29. package/dist/nodes/base.d.ts.map +1 -1
  30. package/dist/nodes/index.d.ts +1 -0
  31. package/dist/nodes/index.d.ts.map +1 -1
  32. package/dist/nodes/index.js +1 -0
  33. package/dist/nodes/index.js.map +1 -1
  34. package/dist/nodes/tool.d.ts.map +1 -1
  35. package/dist/nodes/tool.js +3 -1
  36. package/dist/nodes/tool.js.map +1 -1
  37. package/dist/saga/retry.d.ts +5 -0
  38. package/dist/saga/retry.d.ts.map +1 -1
  39. package/dist/saga/retry.js +13 -0
  40. package/dist/saga/retry.js.map +1 -1
  41. package/dist/scheduler.d.ts +16 -1
  42. package/dist/scheduler.d.ts.map +1 -1
  43. package/dist/scheduler.js +82 -6
  44. package/dist/scheduler.js.map +1 -1
  45. package/dist/subworkflows/subworkflow-node.d.ts.map +1 -1
  46. package/dist/subworkflows/subworkflow-node.js +26 -9
  47. package/dist/subworkflows/subworkflow-node.js.map +1 -1
  48. package/dist/timers/cron-parser.d.ts.map +1 -1
  49. package/dist/timers/cron-parser.js +14 -19
  50. package/dist/timers/cron-parser.js.map +1 -1
  51. package/dist/timers/timer-node.d.ts.map +1 -1
  52. package/dist/timers/timer-node.js.map +1 -1
  53. package/dist/timers/timer-store.d.ts.map +1 -1
  54. package/dist/timers/timer-store.js +1 -1
  55. package/dist/timers/timer-store.js.map +1 -1
  56. package/dist/triggers/trigger-manager.d.ts.map +1 -1
  57. package/dist/triggers/trigger-manager.js +15 -5
  58. package/dist/triggers/trigger-manager.js.map +1 -1
  59. package/package.json +13 -11
package/README.md CHANGED
@@ -3,999 +3,566 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@cogitator-ai/workflows.svg)](https://www.npmjs.com/package/@cogitator-ai/workflows)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- DAG-based workflow engine for Cogitator agents. Build complex multi-step workflows with branching, loops, checkpoints, human-in-the-loop, timers, and more.
6
+ DAG-based workflow engine for Cogitator agents. Build multi-step workflows with branching, loops, joins, checkpoints, human-in-the-loop steps, timers, sagas, subworkflows, map-reduce, triggers and observability.
7
7
 
8
8
  ## Installation
9
9
 
10
10
  ```bash
11
- pnpm add @cogitator-ai/workflows
11
+ pnpm add @cogitator-ai/workflows @cogitator-ai/core
12
12
  ```
13
13
 
14
14
  ## Features
15
15
 
16
- - **DAG Builder** — Type-safe workflow construction with nodes, conditionals, loops
17
- - **Real-time Streaming** — Stream execution events via async generators
18
- - **Checkpoints** — Save and resume workflow state
19
- - **Pre-built Nodes** — Agent, tool, and function nodes
20
- - **Timer System** — Delays, cron schedules, wait-until patterns
21
- - **Saga Patterns** — Retries, circuit breakers, compensation, DLQ
22
- - **Subworkflows** — Nested, parallel, fan-out/fan-in patterns
23
- - **Human-in-the-Loop** — Approvals, choices, inputs, rating
24
- - **Map-Reduce** — Parallel processing with aggregation
25
- - **Triggers** — Cron, webhook, and event triggers
26
- - **Observability** — Tracing and metrics with multiple exporters
16
+ - **DAG Builder** — nodes, conditionals, loops and parallel fan-out with validation and entry-point detection
17
+ - **Correct joins** — a node with several upstream branches runs once, after all of them finished
18
+ - **Node policies** — per-node `timeout`, `retries` and `retryDelay`
19
+ - **Real-time Streaming** — async generator of execution events
20
+ - **Checkpoints** — save and resume workflow state
21
+ - **Pre-built Nodes** — agent, tool, function and custom nodes, plus adapters for timers, human approvals, map-reduce and subworkflows
22
+ - **Saga Patterns** — retries, circuit breakers, compensation, dead-letter queue, idempotency
23
+ - **Triggers** — cron, webhook and event triggers
24
+ - **Observability** — tracing (console, OTLP, Zipkin) and metrics (Prometheus format)
25
+ - **Workflow Management** — run store, scheduling, cancellation, retries, replay
27
26
 
28
27
  ## Quick Start
29
28
 
30
29
  ```typescript
31
- import { WorkflowBuilder, WorkflowExecutor, agentNode } from '@cogitator-ai/workflows';
32
30
  import { Cogitator, Agent } from '@cogitator-ai/core';
31
+ import { WorkflowBuilder, WorkflowExecutor, agentNode } from '@cogitator-ai/workflows';
32
+
33
+ type ReportState = {
34
+ topic: string;
35
+ analysis?: string;
36
+ };
33
37
 
34
- const cogitator = new Cogitator({
35
- /* config */
38
+ const cogitator = new Cogitator({ llm: { defaultModel: 'ollama/llama3.2' } });
39
+ const analyst = new Agent({
40
+ name: 'analyst',
41
+ model: 'ollama/llama3.2',
42
+ instructions: 'Analyze topics concisely.',
36
43
  });
37
- const analyst = new Agent({ name: 'analyst', model: 'openai/gpt-4o', instructions: '...' });
38
44
 
39
- const workflow = new WorkflowBuilder('data-pipeline')
40
- .addNode('analyze', agentNode(analyst))
41
- .addNode('report', async (ctx) => ({ output: `Report: ${ctx.state.analysis}` }))
45
+ const workflow = new WorkflowBuilder<ReportState>('report')
46
+ .initialState({ topic: '' })
47
+ .addNode(
48
+ 'analyze',
49
+ agentNode<ReportState>(analyst, {
50
+ inputMapper: (state) => `Analyze: ${state.topic}`,
51
+ stateMapper: (result) => ({ analysis: result.output }),
52
+ })
53
+ )
54
+ .addNode('report', async (ctx) => ({ output: `Report: ${ctx.state.analysis}` }), {
55
+ after: ['analyze'],
56
+ })
42
57
  .build();
43
58
 
44
- const executor = new WorkflowExecutor(cogitator);
45
- const result = await executor.execute(workflow, { input: 'Analyze this data...' });
59
+ const result = await new WorkflowExecutor(cogitator).execute(workflow, { topic: 'Edge AI' });
60
+ console.log(result.state.analysis, result.error);
46
61
  ```
47
62
 
48
63
  ---
49
64
 
50
- ## Table of Contents
51
-
52
- - [Core Concepts](#core-concepts)
53
- - [Real-time Streaming](#real-time-streaming)
54
- - [Pre-built Nodes](#pre-built-nodes)
55
- - [Conditional Branching](#conditional-branching)
56
- - [Loops](#loops)
57
- - [Checkpoints](#checkpoints)
58
- - [Timer System](#timer-system)
59
- - [Saga Patterns](#saga-patterns)
60
- - [Subworkflows](#subworkflows)
61
- - [Human-in-the-Loop](#human-in-the-loop)
62
- - [Map-Reduce Patterns](#map-reduce-patterns)
63
- - [Triggers](#triggers)
64
- - [Observability](#observability)
65
- - [Workflow Management](#workflow-management)
66
-
67
- ---
68
-
69
65
  ## Core Concepts
70
66
 
71
67
  ### WorkflowBuilder
72
68
 
73
69
  ```typescript
74
- import { WorkflowBuilder } from '@cogitator-ai/workflows';
75
-
76
- const workflow = new WorkflowBuilder<MyState>('my-workflow')
70
+ const workflow = new WorkflowBuilder<{ count: number }>('counter')
77
71
  .initialState({ count: 0 })
78
- .addNode('step1', async (ctx) => ({
79
- state: { ...ctx.state, count: ctx.state.count + 1 },
80
- }))
81
- .addNode(
82
- 'step2',
83
- async (ctx) => ({
84
- output: `Count: ${ctx.state.count}`,
85
- }),
86
- { after: ['step1'] }
87
- )
72
+ .addNode('increment', async (ctx) => ({ state: { count: ctx.state.count + 1 } }))
73
+ .addNode('print', async (ctx) => ({ output: `Count: ${ctx.state.count}` }), {
74
+ after: ['increment'],
75
+ config: { timeout: 5_000, retries: 2, retryDelay: 500 },
76
+ })
88
77
  .build();
89
78
  ```
90
79
 
80
+ - State types must be assignable to `Record<string, unknown>` — declare them with `type` rather than `interface`.
81
+ - `addNode(name, fnOrNode, { after, config })` accepts a node function or a node created by a factory.
82
+ - The entry point is the single node (or conditional/parallel/loop construct) without `after`. Independent roots are rejected — add a parallel fan-out or call `.entryPoint(name)`.
83
+ - Node functions receive `ctx.state` (a copy), `ctx.input` (outputs of upstream nodes), `ctx.nodeId`, `ctx.workflowId`, `ctx.step`, `ctx.reportProgress()` and — for built-in nodes — `cogitator`, `signal` (run cancellation) and `depth` (subworkflow nesting).
84
+ - Return `{ state }` to merge into the workflow state, `{ output }` for downstream nodes and `{ next }` to route dynamically.
85
+
91
86
  ### WorkflowExecutor
92
87
 
93
88
  ```typescript
94
- import { WorkflowExecutor } from '@cogitator-ai/workflows';
95
-
96
89
  const executor = new WorkflowExecutor(cogitator);
97
- const result = await executor.execute(workflow, {
98
- input: 'Start the workflow',
99
- context: { userId: '123' },
100
- timeout: 60000,
101
- });
102
90
 
103
- console.log(result.output);
104
- console.log(result.state);
105
- console.log(result.events);
106
- ```
91
+ const result = await executor.execute(
92
+ workflow,
93
+ { count: 5 },
94
+ {
95
+ maxConcurrency: 4, // parallel nodes per step
96
+ maxIterations: 100, // guards loops
97
+ signal: abortController.signal,
98
+ onNodeStart: (node) => console.log('start', node),
99
+ onNodeComplete: (node, output, duration) => console.log('done', node, duration),
100
+ onNodeError: (node, error) => console.error(node, error.message),
101
+ }
102
+ );
107
103
 
108
- ---
104
+ console.log(result.state, result.nodeResults, result.duration, result.error);
105
+ ```
109
106
 
110
- ## Real-time Streaming
107
+ `execute()` never throws for node failures: the failure is returned in `result.error` (`NodeTimeoutError` for node timeouts).
111
108
 
112
- Stream workflow execution events in real-time using async generators:
109
+ Run-level policies apply to every node:
113
110
 
114
111
  ```typescript
115
- import { WorkflowExecutor } from '@cogitator-ai/workflows';
112
+ await executor.execute(workflow, input, {
113
+ defaultRetry: { maxRetries: 3, backoff: 'exponential', initialDelay: 500 }, // nodes without config.retries
114
+ defaultCircuitBreaker: breakerConfig, // per-node breaker, shared across runs of this executor
115
+ deadLetterQueue: createInMemoryDLQ(), // entry for every node that finally failed
116
+ idempotencyStore, // reuse results of nodes that already completed for this workflowId
117
+ approvalStore, // default store for humanWorkflowNode
118
+ timerStore, // default store for persisted timers
119
+ tracer,
120
+ metricsCollector,
121
+ });
122
+ ```
116
123
 
117
- const executor = new WorkflowExecutor(cogitator);
124
+ ---
118
125
 
119
- for await (const event of executor.stream(workflow)) {
126
+ ## Real-time Streaming
127
+
128
+ ```typescript
129
+ for await (const event of executor.stream(workflow, { count: 0 })) {
120
130
  switch (event.type) {
121
131
  case 'workflow_started':
122
- console.log('Workflow started:', event.workflowId);
123
- break;
124
-
125
132
  case 'node_started':
126
- console.log(`Node ${event.nodeId} started`);
127
- break;
128
-
129
133
  case 'node_progress':
130
- console.log(`Node ${event.nodeId}: ${event.progress}%`);
131
- break;
132
-
133
134
  case 'node_completed':
134
- console.log(`Node ${event.nodeId} completed:`, event.result);
135
- break;
136
-
137
135
  case 'node_error':
138
- console.error(`Node ${event.nodeId} failed:`, event.error);
136
+ console.log(event.type);
139
137
  break;
140
-
141
138
  case 'workflow_completed':
142
- console.log('Workflow completed:', event.result);
139
+ console.log('final state', event.result.state);
143
140
  break;
144
141
  }
145
142
  }
146
143
  ```
147
144
 
148
- ### Event Types
149
-
150
- | Event | Description | Properties |
151
- | -------------------- | -------------------------- | ------------------------------ |
152
- | `workflow_started` | Workflow execution begins | `workflowId`, `timestamp` |
153
- | `node_started` | Node execution begins | `nodeId`, `timestamp` |
154
- | `node_progress` | Progress update from node | `nodeId`, `progress` (0-100) |
155
- | `node_completed` | Node finished successfully | `nodeId`, `result`, `duration` |
156
- | `node_error` | Node execution failed | `nodeId`, `error` |
157
- | `workflow_completed` | Workflow finished | `result`, `duration` |
158
-
159
- ### Reporting Progress from Nodes
160
-
161
- Use `ctx.reportProgress()` inside nodes to emit progress events:
162
-
163
- ```typescript
164
- const workflow = new WorkflowBuilder('processing')
165
- .addNode('process', async (ctx) => {
166
- const items = ctx.state.items;
167
-
168
- for (let i = 0; i < items.length; i++) {
169
- await processItem(items[i]);
170
- ctx.reportProgress?.(Math.round(((i + 1) / items.length) * 100));
171
- }
172
-
173
- return { output: 'Done' };
174
- })
175
- .build();
176
- ```
177
-
178
- ### Progress Callback
179
-
180
- For non-streaming execution, use the `onNodeProgress` callback:
181
-
182
- ```typescript
183
- const result = await executor.execute(workflow, {
184
- input: 'Start',
185
- onNodeProgress: (nodeId, progress) => {
186
- console.log(`${nodeId}: ${progress}%`);
187
- },
188
- });
189
- ```
145
+ Nodes report progress with `ctx.reportProgress(0..100)`; `execute()` exposes it through `onNodeProgress`.
190
146
 
191
147
  ---
192
148
 
193
149
  ## Pre-built Nodes
194
150
 
195
- ### agentNode
196
-
197
- Run an agent as a workflow node:
198
-
199
151
  ```typescript
200
- import { agentNode } from '@cogitator-ai/workflows';
152
+ import { agentNode, toolNode, functionNode, customNode } from '@cogitator-ai/workflows';
201
153
 
202
- const workflow = new WorkflowBuilder('agent-flow')
154
+ builder
203
155
  .addNode(
204
156
  'research',
205
- agentNode(researchAgent, {
206
- promptKey: 'researchPrompt', // State key for input
207
- outputKey: 'researchResult', // State key for output
208
- timeout: 30000,
209
- onToolCall: (call) => console.log('Tool:', call.name),
157
+ agentNode<MyState>(researcher, {
158
+ inputMapper: (state) => state.question,
159
+ stateMapper: (result) => ({ findings: result.output }),
160
+ runOptions: { timeout: 60_000 },
210
161
  })
211
162
  )
212
- .build();
213
- ```
214
-
215
- ### toolNode
216
-
217
- Execute a tool directly:
218
-
219
- ```typescript
220
- import { toolNode } from '@cogitator-ai/workflows';
221
-
222
- const workflow = new WorkflowBuilder('tool-flow')
223
- .addNode('calculate', toolNode('calculator', { expression: '2 + 2' }))
224
- .build();
225
- ```
226
-
227
- ### functionNode
228
-
229
- Custom function as a node:
230
-
231
- ```typescript
232
- import { functionNode } from '@cogitator-ai/workflows';
233
-
234
- const workflow = new WorkflowBuilder('func-flow')
235
163
  .addNode(
236
- 'transform',
237
- functionNode(async (ctx) => {
238
- const transformed = processData(ctx.state.data);
239
- return { state: { ...ctx.state, transformed } };
240
- })
164
+ 'calculate',
165
+ toolNode<MyState, { expression: string }>(calculator, {
166
+ argsMapper: (state) => ({ expression: state.formula }),
167
+ stateMapper: (result) => ({ value: result }),
168
+ }),
169
+ { after: ['research'] }
241
170
  )
242
- .build();
171
+ .addNode(
172
+ 'normalize',
173
+ functionNode<MyState, string>('normalize', async (state) => state.findings!.trim(), {
174
+ stateMapper: (output) => ({ findings: output as string }),
175
+ }),
176
+ { after: ['calculate'] }
177
+ );
243
178
  ```
244
179
 
180
+ Agent and tool nodes use the run's abort signal, so cancelling the workflow cancels in-flight LLM and tool calls.
181
+
245
182
  ---
246
183
 
247
- ## Conditional Branching
184
+ ## Branching, Loops and Joins
248
185
 
249
186
  ```typescript
250
- const workflow = new WorkflowBuilder('approval-flow')
251
- .addNode('review', reviewNode)
252
- .addConditional('check', (state) => state.approved, {
187
+ const workflow = new WorkflowBuilder<{ approved: boolean }>('review-flow')
188
+ .initialState({ approved: false })
189
+ .addNode('review', reviewFn)
190
+ .addConditional('check', (state) => (state.approved ? 'publish' : 'revise'), {
253
191
  after: ['review'],
254
192
  })
255
- .addNode('approve', approveNode, { after: ['check:true'] })
256
- .addNode('reject', rejectNode, { after: ['check:false'] })
257
- .addNode('notify', notifyNode, { after: ['approve', 'reject'] })
193
+ .addNode('publish', publishFn, { after: ['check'] })
194
+ .addNode('revise', reviseFn, { after: ['check'] })
195
+ .addNode('notify', notifyFn, { after: ['publish', 'revise'] })
258
196
  .build();
259
197
  ```
260
198
 
261
- ---
262
-
263
- ## Loops
199
+ A conditional returns the name(s) of the branch(es) to take; every node (or construct) with `after: ['check']` is a candidate branch.
264
200
 
265
201
  ```typescript
266
- const workflow = new WorkflowBuilder('retry-flow')
267
- .addNode('attempt', attemptNode)
268
- .addLoop('retry-check', {
269
- condition: (state) => !state.success && state.attempts < 3,
202
+ const workflow = new WorkflowBuilder<{ attempts: number; done: boolean }>('retry-flow')
203
+ .initialState({ attempts: 0, done: false })
204
+ .addNode('attempt', async (ctx) => ({
205
+ state: { attempts: ctx.state.attempts + 1, done: await tryIt() },
206
+ }))
207
+ .addLoop('again', {
208
+ condition: (state) =>
209
+ !(state as { done: boolean }).done && (state as { attempts: number }).attempts < 3,
270
210
  back: 'attempt',
271
- exit: 'done',
211
+ exit: 'finish',
272
212
  after: ['attempt'],
273
213
  })
274
- .addNode('done', doneNode)
214
+ .addNode('finish', finishFn)
275
215
  .build();
276
216
  ```
277
217
 
278
- ---
279
-
280
- ## Checkpoints
281
-
282
- Save and resume workflow execution:
218
+ Only the loop's `back` and `exit` nodes may follow it; any other node with the loop in `after` makes `build()` throw. A root `addParallel` is the workflow's entry point:
283
219
 
284
220
  ```typescript
285
- import { FileCheckpointStore, InMemoryCheckpointStore } from '@cogitator-ai/workflows';
286
-
287
- // File-based persistence
288
- const store = new FileCheckpointStore('./checkpoints');
289
-
290
- // Execute with checkpoints
291
- await executor.execute(workflow, {
292
- checkpointStore: store,
293
- checkpointInterval: 5000, // Save every 5 seconds
294
- });
295
-
296
- // Resume from checkpoint
297
- const result = await executor.resume(checkpointId, store);
221
+ const workflow = new WorkflowBuilder('fan-out')
222
+ .addParallel('fan', ['fetch-a', 'fetch-b'])
223
+ .addNode('fetch-a', fetchA)
224
+ .addNode('fetch-b', fetchB)
225
+ .addNode('normalize-b', normalizeB, { after: ['fetch-b'] })
226
+ .addNode('merge', async (ctx) => ({ output: ctx.input }), { after: ['fetch-a', 'normalize-b'] })
227
+ .build();
298
228
  ```
299
229
 
300
- ---
230
+ `merge` runs once, after both branches finished, and receives both outputs as `ctx.input`.
301
231
 
302
- ## Timer System
232
+ ---
303
233
 
304
- ### Delay Nodes
234
+ ## Checkpoints
305
235
 
306
236
  ```typescript
307
- import { delayNode, dynamicDelayNode, cronWaitNode, untilNode } from '@cogitator-ai/workflows';
237
+ import { FileCheckpointStore } from '@cogitator-ai/workflows';
308
238
 
309
- const workflow = new WorkflowBuilder('timer-flow')
310
- // Fixed delay
311
- .addNode('wait', delayNode(5000)) // 5 seconds
239
+ const executor = new WorkflowExecutor(cogitator, new FileCheckpointStore('./checkpoints'));
312
240
 
313
- // Dynamic delay based on state
314
- .addNode(
315
- 'dynamic-wait',
316
- dynamicDelayNode((state) => state.retryCount * 1000)
317
- )
318
-
319
- // Wait for cron schedule
320
- .addNode('cron-wait', cronWaitNode('0 9 * * *')) // Wait until 9 AM
241
+ const first = await executor.execute(workflow, input, {
242
+ checkpoint: true,
243
+ checkpointStrategy: 'per-node', // or 'per-iteration' (default)
244
+ });
321
245
 
322
- // Wait until specific date
323
- .addNode(
324
- 'until',
325
- untilNode((state) => state.scheduledTime)
326
- )
327
- .build();
246
+ if (first.error && first.checkpointId) {
247
+ const resumed = await executor.resume(workflow, first.checkpointId);
248
+ }
328
249
  ```
329
250
 
330
- ### Duration Parsing
331
-
332
- ```typescript
333
- import { parseDuration, formatDuration } from '@cogitator-ai/workflows';
334
-
335
- const ms = parseDuration('1h30m'); // 5400000
336
- const str = formatDuration(5400000); // '1h 30m'
337
- ```
251
+ ---
338
252
 
339
- ### Cron Utilities
253
+ ## Timers
340
254
 
341
255
  ```typescript
342
256
  import {
343
- validateCronExpression,
344
- getNextCronOccurrence,
345
- getNextCronOccurrences,
346
- describeCronExpression,
347
- CRON_PRESETS,
257
+ delayNode,
258
+ dynamicDelayNode,
259
+ cronWaitNode,
260
+ untilNode,
261
+ timerWorkflowNode,
262
+ parseDuration,
263
+ formatDuration,
348
264
  } from '@cogitator-ai/workflows';
349
265
 
350
- // Validate
351
- const valid = validateCronExpression('0 9 * * 1-5'); // true
352
-
353
- // Get next occurrence
354
- const next = getNextCronOccurrence('0 9 * * *');
355
-
356
- // Get multiple occurrences
357
- const nextFive = getNextCronOccurrences('0 9 * * *', 5);
358
-
359
- // Human-readable description
360
- const desc = describeCronExpression('0 9 * * 1-5'); // "At 09:00 on weekdays"
266
+ builder
267
+ .addNode('cool-down', timerWorkflowNode(delayNode('cool-down', parseDuration('5m'))))
268
+ .addNode(
269
+ 'backoff',
270
+ timerWorkflowNode(dynamicDelayNode<MyState>('backoff', (state) => state.retries * 1000)),
271
+ { after: ['cool-down'] }
272
+ )
273
+ .addNode('business-hours', timerWorkflowNode(cronWaitNode('business-hours', '0 9 * * 1-5')), {
274
+ after: ['backoff'],
275
+ })
276
+ .addNode('deadline', timerWorkflowNode(untilNode<MyState>('deadline', (state) => state.dueAt)), {
277
+ after: ['business-hours'],
278
+ });
361
279
 
362
- // Presets
363
- CRON_PRESETS.EVERY_MINUTE; // '* * * * *'
364
- CRON_PRESETS.HOURLY; // '0 * * * *'
365
- CRON_PRESETS.DAILY; // '0 0 * * *'
366
- CRON_PRESETS.WEEKLY; // '0 0 * * 0'
367
- CRON_PRESETS.MONTHLY; // '0 0 1 * *'
280
+ formatDuration(90_000); // '1.5m'
368
281
  ```
369
282
 
370
- ### TimerManager
371
-
372
- Manage recurring timers:
373
-
374
- ```typescript
375
- import { createTimerManager, createRecurringScheduler } from '@cogitator-ai/workflows';
376
-
377
- const manager = createTimerManager({
378
- maxConcurrent: 10,
379
- defaultTimeout: 60000,
380
- });
381
-
382
- // One-shot timer
383
- manager.schedule('task-1', 5000, async () => {
384
- console.log('Executed after 5 seconds');
385
- });
386
-
387
- // Recurring timer
388
- const scheduler = createRecurringScheduler();
389
- scheduler.schedule('daily-report', '0 9 * * *', async () => {
390
- await generateDailyReport();
391
- });
392
- ```
283
+ Waits are cancelled when the run is aborted. Pass `{ timerStore }` to `timerWorkflowNode` together with `persist: true` configs (or use `createTimerNodeHelpers(store)`) to persist timers; `createTimerManager(store)` and `createRecurringScheduler(manager)` process persisted and recurring timers. Cron helpers: `validateCronExpression`, `getNextCronOccurrence(s)`, `describeCronExpression`, `CRON_PRESETS`.
393
284
 
394
285
  ---
395
286
 
396
287
  ## Saga Patterns
397
288
 
398
- ### Retry with Backoff
289
+ ### Retry
399
290
 
400
291
  ```typescript
401
- import { executeWithRetry, withRetry, Retryable } from '@cogitator-ai/workflows';
402
-
403
- // Function wrapper
404
- const result = await executeWithRetry(async () => await unreliableOperation(), {
405
- maxAttempts: 5,
406
- initialDelay: 1000,
407
- maxDelay: 30000,
408
- backoffMultiplier: 2,
292
+ import { executeWithRetry, withRetry } from '@cogitator-ai/workflows';
293
+
294
+ const outcome = await executeWithRetry((attempt) => callService(attempt), {
295
+ maxRetries: 4,
296
+ backoff: 'exponential', // 'constant' | 'linear' | 'exponential'
297
+ initialDelay: 500,
298
+ maxDelay: 10_000,
409
299
  jitter: 0.1,
410
- shouldRetry: (error) => error.code !== 'FATAL',
411
- onRetry: (attempt, error, delay) => console.log(`Retry ${attempt} in ${delay}ms`),
300
+ isRetryable: (error) => !error.message.includes('invalid'),
412
301
  });
302
+ if (!outcome.success) console.error(outcome.error);
413
303
 
414
- // Decorator-style
415
- const retryableFetch = withRetry({ maxAttempts: 3 })(async (url: string) => await fetch(url));
416
-
417
- // Class decorator
418
- class ApiClient {
419
- @Retryable({ maxAttempts: 3, initialDelay: 500 })
420
- async request(endpoint: string) {
421
- return fetch(endpoint);
422
- }
423
- }
304
+ const fetchWithRetry = withRetry((url: string) => fetch(url), { maxRetries: 3 });
424
305
  ```
425
306
 
426
307
  ### Circuit Breaker
427
308
 
428
309
  ```typescript
429
- import { CircuitBreaker, createCircuitBreaker, WithCircuitBreaker } from '@cogitator-ai/workflows';
430
-
431
- const breaker = createCircuitBreaker({
432
- failureThreshold: 5,
433
- successThreshold: 2,
434
- timeout: 30000,
435
- halfOpenMaxAttempts: 3,
436
- onStateChange: (from, to) => console.log(`Circuit: ${from} -> ${to}`),
437
- });
310
+ import { createCircuitBreaker, CircuitBreakerOpenError } from '@cogitator-ai/workflows';
311
+
312
+ const breaker = createCircuitBreaker({ threshold: 5, resetTimeout: 30_000, successThreshold: 2 });
438
313
 
439
- // Use the breaker
440
314
  try {
441
- const result = await breaker.execute(async () => {
442
- return await externalService.call();
443
- });
315
+ await breaker.execute('payments', () => payments.charge(order));
444
316
  } catch (error) {
445
317
  if (error instanceof CircuitBreakerOpenError) {
446
- console.log('Circuit is open, using fallback');
318
+ // use a fallback
447
319
  }
448
320
  }
449
321
 
450
- // Get stats
451
- const stats = breaker.getStats();
452
- console.log(stats.failures, stats.successes, stats.state);
453
-
454
- // Decorator-style
455
- class ServiceClient {
456
- @WithCircuitBreaker({ failureThreshold: 3 })
457
- async call() {
458
- return fetch('/api');
459
- }
460
- }
322
+ console.log(breaker.getStats('payments'));
461
323
  ```
462
324
 
463
- ### Compensation (Saga)
325
+ ### Compensation
464
326
 
465
327
  ```typescript
466
- import { CompensationManager, compensationBuilder } from '@cogitator-ai/workflows';
467
-
468
- const saga = compensationBuilder<{ orderId: string }>()
469
- .step({
470
- name: 'reserve-inventory',
471
- execute: async (ctx) => {
472
- ctx.state.inventoryReserved = await inventory.reserve(ctx.data.orderId);
473
- },
474
- compensate: async (ctx) => {
475
- await inventory.release(ctx.data.orderId);
476
- },
477
- })
478
- .step({
479
- name: 'charge-payment',
480
- execute: async (ctx) => {
481
- ctx.state.paymentId = await payments.charge(ctx.data.orderId);
482
- },
483
- compensate: async (ctx) => {
484
- await payments.refund(ctx.state.paymentId);
485
- },
486
- })
487
- .step({
488
- name: 'ship-order',
489
- execute: async (ctx) => {
490
- await shipping.ship(ctx.data.orderId);
491
- },
492
- compensate: async (ctx) => {
493
- await shipping.cancel(ctx.data.orderId);
494
- },
495
- })
328
+ import { compensationBuilder } from '@cogitator-ai/workflows';
329
+
330
+ const compensation = compensationBuilder<OrderState>()
331
+ .addStep('reserve', async (state) => inventory.release(state.orderId))
332
+ .addStep('charge', async (state) => payments.refund(state.paymentId!))
496
333
  .build();
497
334
 
498
- const manager = new CompensationManager();
499
- const result = await manager.execute(saga, { orderId: 'order-123' });
335
+ compensation.markCompleted('reserve', reservation);
336
+ compensation.markCompleted('charge', payment);
500
337
 
501
- if (!result.success) {
502
- console.log('Saga failed at:', result.failedStep);
503
- console.log('Compensated steps:', result.compensatedSteps);
504
- }
338
+ const report = await compensation.compensate(state, 'ship', new Error('carrier down'));
505
339
  ```
506
340
 
507
- ### Dead Letter Queue (DLQ)
508
-
509
- ```typescript
510
- import { createFileDLQ, createInMemoryDLQ } from '@cogitator-ai/workflows';
511
-
512
- const dlq = createFileDLQ('./dlq');
513
-
514
- // Add failed item
515
- await dlq.add({
516
- id: 'job-123',
517
- payload: { orderId: 'order-456' },
518
- error: 'Payment failed',
519
- source: 'checkout-workflow',
520
- attemptCount: 3,
521
- });
522
-
523
- // Process DLQ
524
- const items = await dlq.list({ source: 'checkout-workflow' });
525
- for (const item of items) {
526
- try {
527
- await retryJob(item.payload);
528
- await dlq.remove(item.id);
529
- } catch {
530
- await dlq.update(item.id, { attemptCount: item.attemptCount + 1 });
531
- }
532
- }
533
- ```
341
+ Compensations run in reverse completion order for the steps marked completed.
534
342
 
535
- ### Idempotency
343
+ ### Dead Letter Queue and Idempotency
536
344
 
537
345
  ```typescript
538
- import { idempotent, Idempotent, createFileIdempotencyStore } from '@cogitator-ai/workflows';
539
-
540
- const store = createFileIdempotencyStore('./idempotency');
541
-
542
- // Function wrapper
543
- const processOrder = idempotent(store, {
544
- keyGenerator: (orderId: string) => `order:${orderId}`,
545
- ttl: 24 * 60 * 60 * 1000, // 24 hours
546
- })(async (orderId: string) => {
547
- return await processOrderInternal(orderId);
548
- });
346
+ import {
347
+ createFileDLQ,
348
+ createDLQEntry,
349
+ createInMemoryIdempotencyStore,
350
+ idempotent,
351
+ } from '@cogitator-ai/workflows';
549
352
 
550
- // Safe to call multiple times
551
- await processOrder('order-123'); // Executes
552
- await processOrder('order-123'); // Returns cached result
353
+ const dlq = createFileDLQ('./dlq');
354
+ await dlq.add(createDLQEntry('charge', workflowId, 'checkout', state, error, { attempts: 3 }));
355
+ const failed = await dlq.list({ workflowName: 'checkout' });
553
356
 
554
- // Decorator-style
555
- class OrderService {
556
- @Idempotent({ keyGenerator: (id) => `order:${id}`, ttl: 86400000 })
557
- async process(orderId: string) {
558
- return processOrderInternal(orderId);
559
- }
560
- }
357
+ const store = createInMemoryIdempotencyStore();
358
+ const receipt = await idempotent(store, `charge:${orderId}`, () => payments.charge(orderId));
561
359
  ```
562
360
 
563
361
  ---
564
362
 
565
363
  ## Subworkflows
566
364
 
567
- ### Nested Subworkflows
568
-
569
365
  ```typescript
570
- import { subworkflowNode, executeSubworkflow } from '@cogitator-ai/workflows';
366
+ import {
367
+ subworkflowNode,
368
+ subworkflowWorkflowNode,
369
+ fanOutFanIn,
370
+ parallelSubworkflowsNode,
371
+ } from '@cogitator-ai/workflows';
571
372
 
572
- const mainWorkflow = new WorkflowBuilder('main')
573
- .addNode('prepare', prepareNode)
373
+ builder
574
374
  .addNode(
575
- 'process',
576
- subworkflowNode(processingWorkflow, {
577
- inputMapper: (state) => ({ items: state.items }),
578
- outputMapper: (result) => ({ processedItems: result.output }),
579
- maxDepth: 5,
580
- errorStrategy: 'fail', // 'fail' | 'continue' | 'compensate'
581
- })
375
+ 'enrich',
376
+ subworkflowWorkflowNode(
377
+ subworkflowNode<ParentState, ChildState>('enrich', {
378
+ workflow: enrichmentWorkflow,
379
+ inputMapper: (state) => ({ record: state.record }),
380
+ outputMapper: (result, state) => ({ ...state, enriched: result.state.record }),
381
+ timeout: 60_000,
382
+ onError: 'retry', // 'propagate' | 'ignore' | 'catch' | 'retry'
383
+ maxDepth: 5,
384
+ })
385
+ )
582
386
  )
583
- .addNode('finalize', finalizeNode, { after: ['process'] })
584
- .build();
585
- ```
586
-
587
- ### Parallel Subworkflows
588
-
589
- ```typescript
590
- import { parallelSubworkflows, fanOutFanIn, scatterGather } from '@cogitator-ai/workflows';
591
-
592
- // Fan-out/Fan-in pattern
593
- const workflow = new WorkflowBuilder('parallel')
594
387
  .addNode(
595
- 'distribute',
596
- fanOutFanIn(
597
- [
598
- { workflow: workflowA, input: { type: 'a' } },
599
- { workflow: workflowB, input: { type: 'b' } },
600
- { workflow: workflowC, input: { type: 'c' } },
601
- ],
602
- {
388
+ 'per-region',
389
+ parallelSubworkflowsNode(
390
+ fanOutFanIn<ParentState, RegionState>('per-region', {
391
+ workflow: regionWorkflow,
392
+ getInputs: (state) => state.regions.map((region) => ({ id: region, input: { region } })),
393
+ aggregator: (results, state) => ({ ...state, regionCount: results.size }),
603
394
  concurrency: 3,
604
- onProgress: (completed, total) => console.log(`${completed}/${total}`),
605
- }
606
- )
607
- )
608
- .build();
609
-
610
- // Scatter-Gather (collect all results)
611
- const results = await scatterGather(executor, workflows, inputs);
612
-
613
- // Race (first to complete wins)
614
- const winner = await raceSubworkflows(executor, [workflow1, workflow2]);
615
-
616
- // Fallback (try until one succeeds)
617
- const result = await fallbackSubworkflows(executor, [primary, secondary, tertiary]);
395
+ })
396
+ ),
397
+ { after: ['enrich'] }
398
+ );
618
399
  ```
619
400
 
401
+ Child failures propagate to the parent (or follow `onError`); timeouts cancel the child run. `scatterGather`, `raceSubworkflows` and `fallbackSubworkflows` cover the other common patterns.
402
+
620
403
  ---
621
404
 
622
405
  ## Human-in-the-Loop
623
406
 
624
- ### Approval Node
625
-
626
407
  ```typescript
627
- import { approvalNode, InMemoryApprovalStore, WebhookNotifier } from '@cogitator-ai/workflows';
628
-
629
- const store = new InMemoryApprovalStore();
630
- const notifier = new WebhookNotifier('https://slack.webhook.url');
631
-
632
- const workflow = new WorkflowBuilder('approval-flow')
633
- .addNode(
634
- 'request',
635
- approvalNode({
636
- message: (state) => `Approve expense: $${state.amount}`,
637
- approvers: ['manager@company.com'],
638
- timeout: 24 * 60 * 60 * 1000, // 24 hours
639
- store,
640
- notifier,
641
- })
642
- )
643
- .addConditional('check', (state) => state.approved, { after: ['request'] })
644
- .addNode('process', processNode, { after: ['check:true'] })
645
- .addNode('reject', rejectNode, { after: ['check:false'] })
646
- .build();
647
- ```
408
+ import {
409
+ approvalNode,
410
+ humanWorkflowNode,
411
+ InMemoryApprovalStore,
412
+ WebhookNotifier,
413
+ } from '@cogitator-ai/workflows';
648
414
 
649
- ### Choice Node
415
+ const approvalStore = new InMemoryApprovalStore();
416
+ const approvalNotifier = new WebhookNotifier({ url: 'https://hooks.example.com/approvals' });
650
417
 
651
- ```typescript
652
- import { choiceNode } from '@cogitator-ai/workflows';
653
-
654
- const workflow = new WorkflowBuilder('choice-flow')
418
+ builder
655
419
  .addNode(
656
- 'select',
657
- choiceNode({
658
- message: 'Select processing method:',
659
- choices: [
660
- { id: 'fast', label: 'Fast (less accurate)', value: 'fast' },
661
- { id: 'accurate', label: 'Accurate (slower)', value: 'accurate' },
662
- ],
663
- store,
664
- notifier,
665
- })
420
+ 'approve-expense',
421
+ humanWorkflowNode(
422
+ approvalNode<ExpenseState>('approve-expense', {
423
+ title: 'Approve expense',
424
+ description: (state) => `Amount: $${state.amount}`,
425
+ assignee: 'manager@company.com',
426
+ timeout: 24 * 60 * 60 * 1000,
427
+ timeoutAction: 'reject',
428
+ }),
429
+ { approvalStore, approvalNotifier, stateMapper: (result) => ({ approved: result.approved }) }
430
+ )
666
431
  )
667
- .build();
668
- ```
669
-
670
- ### Input Node
671
-
672
- ```typescript
673
- import { inputNode } from '@cogitator-ai/workflows';
432
+ .addConditional('route', (state) => (state.approved ? 'pay' : 'decline'), {
433
+ after: ['approve-expense'],
434
+ });
674
435
 
675
- const workflow = new WorkflowBuilder('input-flow')
676
- .addNode(
677
- 'get-details',
678
- inputNode({
679
- message: 'Please provide additional details:',
680
- fields: [
681
- { name: 'reason', type: 'text', required: true },
682
- { name: 'priority', type: 'select', options: ['low', 'medium', 'high'] },
683
- ],
684
- store,
685
- notifier,
686
- })
687
- )
688
- .build();
436
+ // Elsewhere (API handler, UI, Slack action):
437
+ await approvalStore.submitResponse({
438
+ requestId,
439
+ decision: true,
440
+ respondedBy: 'manager@company.com',
441
+ respondedAt: Date.now(),
442
+ });
689
443
  ```
690
444
 
691
- ### Approval Chains
692
-
693
- ```typescript
694
- import { managementChain, chainNode } from '@cogitator-ai/workflows';
695
-
696
- const workflow = new WorkflowBuilder('chain-approval')
697
- .addNode(
698
- 'approval',
699
- managementChain({
700
- steps: [
701
- { approver: 'team-lead@co.com', requiredFor: (state) => state.amount > 100 },
702
- { approver: 'manager@co.com', requiredFor: (state) => state.amount > 1000 },
703
- { approver: 'director@co.com', requiredFor: (state) => state.amount > 10000 },
704
- ],
705
- store,
706
- notifier,
707
- })
708
- )
709
- .build();
710
- ```
445
+ Other configs: `choiceNode`, `inputNode`, `ratingNode`, `chainNode`, `managementChain`. Notifiers: `ConsoleNotifier`, `WebhookNotifier`, `slackNotifier`, `CompositeNotifier`, `filteredNotifier`, `priorityRouter`. `FileApprovalStore` persists requests.
711
446
 
712
447
  ---
713
448
 
714
- ## Map-Reduce Patterns
715
-
716
- ### Map (Parallel Processing)
717
-
718
- ```typescript
719
- import { mapNode, parallelMap, batchedMap } from '@cogitator-ai/workflows';
720
-
721
- const workflow = new WorkflowBuilder('map-flow')
722
- .addNode(
723
- 'process-items',
724
- mapNode({
725
- items: (state) => state.items,
726
- mapper: async (item, index, ctx) => {
727
- return await processItem(item);
728
- },
729
- concurrency: 5,
730
- onProgress: ({ completed, total }) => console.log(`${completed}/${total}`),
731
- })
732
- )
733
- .build();
734
-
735
- // Batched processing
736
- const results = await batchedMap(items, processItem, { batchSize: 10, concurrency: 3 });
737
- ```
738
-
739
- ### Reduce (Aggregation)
449
+ ## Map-Reduce
740
450
 
741
451
  ```typescript
742
- import { reduceNode, collect, sum, groupBy, stats } from '@cogitator-ai/workflows';
452
+ import { mapReduceNode, mapReduceWorkflowNode } from '@cogitator-ai/workflows';
743
453
 
744
- const workflow = new WorkflowBuilder('reduce-flow')
745
- .addNode(
746
- 'aggregate',
747
- reduceNode({
748
- items: (state) => state.results,
749
- reducer: (acc, item) => acc + item.value,
750
- initialValue: 0,
751
- })
752
- )
753
- .build();
754
-
755
- // Built-in aggregators
756
- const collected = collect(items); // Collect all
757
- const total = sum(items, (i) => i.value); // Sum values
758
- const grouped = groupBy(items, (i) => i.category); // Group by key
759
- const statistics = stats(items, (i) => i.score); // { min, max, avg, sum, count }
760
- ```
761
-
762
- ### Map-Reduce
763
-
764
- ```typescript
765
- import { mapReduceNode, executeMapReduce } from '@cogitator-ai/workflows';
766
-
767
- const workflow = new WorkflowBuilder('mapreduce-flow')
768
- .addNode(
769
- 'word-count',
770
- mapReduceNode({
771
- items: (state) => state.documents,
772
- mapper: async (doc) => {
773
- const words = doc.text.split(/\s+/);
774
- return words.map((w) => ({ word: w, count: 1 }));
454
+ builder.addNode(
455
+ 'score-documents',
456
+ mapReduceWorkflowNode(
457
+ mapReduceNode<DocsState, number, number>('score-documents', {
458
+ map: {
459
+ items: (state) => state.documents,
460
+ mapper: async (doc) => scoreDocument(doc as string),
461
+ concurrency: 5,
462
+ continueOnError: true,
775
463
  },
776
- reducer: (results) => {
777
- return results.flat().reduce((acc, { word, count }) => {
778
- acc[word] = (acc[word] || 0) + count;
779
- return acc;
780
- }, {});
464
+ reduce: {
465
+ initial: 0,
466
+ reducer: (sum, item) => sum + item.result,
467
+ finalize: (sum, state) => sum / state.documents.length,
781
468
  },
782
- concurrency: 10,
783
- })
469
+ }),
470
+ { stateMapper: (result) => ({ averageScore: result.reduced }) }
784
471
  )
785
- .build();
472
+ );
786
473
  ```
787
474
 
475
+ `mapNode` + `mapWorkflowNode`, `parallelMap`, `batchedMap` and the reducer presets `collect`, `sum`, `count`, `groupBy`, `partition`, `flatMap` and `stats` cover other shapes.
476
+
788
477
  ---
789
478
 
790
479
  ## Triggers
791
480
 
792
- ### Cron Trigger
793
-
794
481
  ```typescript
795
- import { createCronTrigger, CronTriggerExecutor } from '@cogitator-ai/workflows';
796
-
797
- const trigger = createCronTrigger({
798
- expression: '0 9 * * 1-5', // 9 AM on weekdays
799
- timezone: 'America/New_York',
800
- workflow: dailyReportWorkflow,
801
- executor,
802
- onTrigger: (time) => console.log('Triggered at:', time),
803
- });
482
+ import { createTriggerManager, cronTrigger, webhookTrigger } from '@cogitator-ai/workflows';
804
483
 
805
- trigger.start();
806
- // Later: trigger.stop();
807
- ```
808
-
809
- ### Webhook Trigger
810
-
811
- ```typescript
812
- import { createWebhookTrigger, WebhookTriggerExecutor } from '@cogitator-ai/workflows';
813
-
814
- const webhook = createWebhookTrigger({
815
- path: '/webhooks/github',
816
- workflow: githubEventWorkflow,
817
- executor,
818
- auth: {
819
- type: 'hmac',
820
- secret: process.env.WEBHOOK_SECRET!,
821
- header: 'X-Hub-Signature-256',
484
+ const triggers = createTriggerManager({
485
+ onTriggerFire: async (trigger, context) => {
486
+ const run = await manager.schedule(workflows[trigger.workflowName], { input: context.payload });
487
+ return run;
822
488
  },
823
- rateLimit: {
824
- maxRequests: 100,
825
- windowMs: 60000,
826
- },
827
- inputMapper: (req) => ({ event: req.body.action, payload: req.body }),
828
489
  });
490
+ triggers.start();
829
491
 
830
- // Handle incoming request
831
- const result = await webhook.handle(request);
832
- ```
833
-
834
- ### Trigger Manager
835
-
836
- ```typescript
837
- import {
838
- createTriggerManager,
839
- cronTrigger,
840
- webhookTrigger,
841
- eventTrigger,
842
- } from '@cogitator-ai/workflows';
843
-
844
- const manager = createTriggerManager({ executor });
845
-
846
- manager.register(
847
- 'daily-report',
848
- cronTrigger({
849
- expression: '0 9 * * *',
850
- workflow: reportWorkflow,
851
- })
852
- );
853
-
854
- manager.register(
855
- 'github-webhook',
856
- webhookTrigger({
857
- path: '/hooks/github',
858
- workflow: githubWorkflow,
859
- })
860
- );
492
+ await triggers.register({
493
+ workflowName: 'daily-report',
494
+ type: 'cron',
495
+ config: cronTrigger('0 9 * * *', { timezone: 'Europe/Berlin' }),
496
+ enabled: true,
497
+ });
861
498
 
862
- manager.register(
863
- 'order-created',
864
- eventTrigger({
865
- event: 'order.created',
866
- workflow: orderProcessingWorkflow,
867
- })
868
- );
499
+ await triggers.register({
500
+ workflowName: 'github-sync',
501
+ type: 'webhook',
502
+ config: webhookTrigger('/hooks/github', 'POST', {
503
+ auth: { type: 'hmac', secret: process.env.GH_SECRET! },
504
+ }),
505
+ enabled: true,
506
+ });
869
507
 
870
- await manager.startAll();
508
+ // In your HTTP handler:
509
+ const response = await triggers.handleWebhook({
510
+ path: req.path,
511
+ method: req.method,
512
+ headers,
513
+ body,
514
+ });
871
515
  ```
872
516
 
873
517
  ---
874
518
 
875
519
  ## Observability
876
520
 
877
- ### Tracing
878
-
879
521
  ```typescript
880
- import {
881
- createTracer,
882
- OTLPSpanExporter,
883
- ZipkinSpanExporter,
884
- CompositeSpanExporter,
885
- } from '@cogitator-ai/workflows';
886
-
887
- // OTLP exporter (Jaeger, Tempo, etc.)
888
- const otlpExporter = new OTLPSpanExporter({
889
- endpoint: 'http://localhost:4318/v1/traces',
890
- headers: { 'X-Api-Key': 'secret' },
891
- });
892
-
893
- // Zipkin exporter
894
- const zipkinExporter = new ZipkinSpanExporter({
895
- endpoint: 'http://localhost:9411/api/v2/spans',
896
- });
897
-
898
- // Composite (multiple exporters)
899
- const exporter = new CompositeSpanExporter([otlpExporter, zipkinExporter]);
522
+ import { createTracer, createMetricsCollector } from '@cogitator-ai/workflows';
900
523
 
901
524
  const tracer = createTracer({
902
- serviceName: 'my-workflow-service',
903
- exporter,
904
- });
905
-
906
- // Execute with tracing
907
- await executor.execute(workflow, { tracer });
908
- ```
909
-
910
- ### Metrics
911
-
912
- ```typescript
913
- import { createMetricsCollector, WorkflowMetricsCollector } from '@cogitator-ai/workflows';
914
-
915
- const metrics = createMetricsCollector({
916
- prefix: 'cogitator_workflow',
917
- labels: { environment: 'production' },
525
+ enabled: true,
526
+ serviceName: 'billing-workflows',
527
+ exporter: 'otlp', // 'console' | 'otlp' | 'jaeger' | 'zipkin'
528
+ exporterEndpoint: 'http://localhost:4318/v1/traces',
918
529
  });
530
+ const metricsCollector = createMetricsCollector({ prefix: 'cogitator_workflow' });
919
531
 
920
- // Execute with metrics
921
- await executor.execute(workflow, { metrics });
532
+ await executor.execute(workflow, input, { tracer, metricsCollector });
533
+ await tracer.flush();
922
534
 
923
- // Get metrics
924
- const nodeMetrics = metrics.getNodeMetrics('my-node');
925
- console.log(nodeMetrics.executionCount);
926
- console.log(nodeMetrics.averageDuration);
927
- console.log(nodeMetrics.errorRate);
928
-
929
- const workflowMetrics = metrics.getWorkflowMetrics('my-workflow');
930
- console.log(workflowMetrics.completionRate);
931
- console.log(workflowMetrics.averageCompletionTime);
535
+ console.log(metricsCollector.getWorkflowMetrics('report'));
536
+ console.log(metricsCollector.toPrometheusFormat());
932
537
  ```
933
538
 
539
+ The executor creates one workflow span and a child span per node execution (parallel nodes keep correct parents) and records workflow/node counts, latencies and retries.
540
+
934
541
  ---
935
542
 
936
543
  ## Workflow Management
937
544
 
938
- ### WorkflowManager
939
-
940
545
  ```typescript
941
546
  import { createWorkflowManager, createFileRunStore } from '@cogitator-ai/workflows';
942
547
 
943
- const runStore = createFileRunStore('./runs');
944
-
945
548
  const manager = createWorkflowManager({
946
- executor,
947
- runStore,
948
- concurrency: 10,
949
- defaultTimeout: 300000,
549
+ cogitator,
550
+ runStore: createFileRunStore({ directory: './runs' }),
551
+ maxConcurrency: 10,
552
+ defaultTimeout: 300_000, // runs exceeding it are cancelled and marked failed
553
+ tracer,
554
+ metrics: metricsCollector,
950
555
  });
556
+ manager.start();
951
557
 
952
- // Schedule a workflow run
953
- const runId = await manager.schedule(workflow, {
954
- input: 'Process this',
955
- priority: 1,
956
- scheduledAt: new Date(Date.now() + 60000), // 1 minute from now
957
- tags: ['daily', 'report'],
958
- });
558
+ const result = await manager.execute(workflow, input, { tags: ['nightly'] });
959
559
 
960
- // Get run status
961
- const run = await manager.getRun(runId);
962
- console.log(run.status); // 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
963
-
964
- // List runs
965
- const runs = await manager.listRuns({
966
- status: 'running',
967
- workflowId: 'daily-report',
968
- fromDate: new Date('2024-01-01'),
969
- });
970
-
971
- // Cancel a run
560
+ const runId = await manager.schedule(workflow, { at: Date.now() + 60_000, input, priority: 1 });
972
561
  await manager.cancel(runId);
973
562
 
974
- // Get stats
975
- const stats = await manager.getStats();
976
- console.log(stats.pending, stats.running, stats.completed, stats.failed);
977
- ```
978
-
979
- ### JobScheduler
980
-
981
- ```typescript
982
- import { createJobScheduler, PriorityQueue } from '@cogitator-ai/workflows';
983
-
984
- const scheduler = createJobScheduler({
985
- concurrency: 5,
986
- maxQueueSize: 1000,
987
- });
988
-
989
- // Add jobs with priority
990
- scheduler.enqueue({ id: 'job-1', payload: data1, priority: 1 });
991
- scheduler.enqueue({ id: 'job-2', payload: data2, priority: 10 }); // Higher priority
992
-
993
- // Process jobs
994
- scheduler.process(async (job) => {
995
- await processJob(job.payload);
996
- });
997
-
998
- scheduler.start();
563
+ const runs = await manager.listRuns({ status: 'failed', workflowName: 'report', limit: 20 });
564
+ const stats = await manager.getStats('report');
565
+ await manager.retry(runs[0].id);
999
566
  ```
1000
567
 
1001
568
  ---