@bratsos/workflow-engine 0.0.11

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.
@@ -0,0 +1,497 @@
1
+ # Runtime Setup
2
+
3
+ Complete guide for configuring and running WorkflowRuntime.
4
+
5
+ ## Creating a Runtime
6
+
7
+ ```typescript
8
+ import { createWorkflowRuntime } from "@bratsos/workflow-engine";
9
+ import {
10
+ createPrismaWorkflowPersistence,
11
+ createPrismaJobQueue,
12
+ createPrismaAICallLogger,
13
+ } from "@bratsos/workflow-engine/persistence/prisma";
14
+ import { PrismaClient } from "@prisma/client";
15
+
16
+ const prisma = new PrismaClient();
17
+
18
+ // PostgreSQL (default)
19
+ const runtime = createWorkflowRuntime({
20
+ // Required
21
+ persistence: createPrismaWorkflowPersistence(prisma),
22
+ jobQueue: createPrismaJobQueue(prisma),
23
+ registry: {
24
+ getWorkflow: (id) => workflowMap[id] ?? null,
25
+ },
26
+
27
+ // Optional
28
+ aiCallLogger: createPrismaAICallLogger(prisma),
29
+ pollIntervalMs: 10000,
30
+ jobPollIntervalMs: 1000,
31
+ staleJobThresholdMs: 60000,
32
+ workerId: "worker-1",
33
+ getWorkflowPriority: (id) => priorityMap[id] ?? 5,
34
+ });
35
+
36
+ // SQLite - pass databaseType to persistence and job queue
37
+ const runtime = createWorkflowRuntime({
38
+ persistence: createPrismaWorkflowPersistence(prisma, { databaseType: "sqlite" }),
39
+ jobQueue: createPrismaJobQueue(prisma, { databaseType: "sqlite" }),
40
+ registry: { getWorkflow: (id) => workflowMap[id] ?? null },
41
+ aiCallLogger: createPrismaAICallLogger(prisma),
42
+ });
43
+ ```
44
+
45
+ ## WorkflowRuntimeConfig
46
+
47
+ ```typescript
48
+ interface WorkflowRuntimeConfig {
49
+ /** Persistence implementation (required) */
50
+ persistence: WorkflowPersistence;
51
+
52
+ /** Job queue implementation (required) */
53
+ jobQueue: JobQueue;
54
+
55
+ /** Workflow registry (required) */
56
+ registry: WorkflowRegistry;
57
+
58
+ /** AI call logger for createAIHelper (optional) */
59
+ aiCallLogger?: AICallLogger;
60
+
61
+ /** Orchestration poll interval in ms (default: 10000) */
62
+ pollIntervalMs?: number;
63
+
64
+ /** Job dequeue interval in ms (default: 1000) */
65
+ jobPollIntervalMs?: number;
66
+
67
+ /** Worker ID (default: auto-generated) */
68
+ workerId?: string;
69
+
70
+ /** Stale job threshold in ms (default: 60000) */
71
+ staleJobThresholdMs?: number;
72
+
73
+ /** Function to determine workflow priority */
74
+ getWorkflowPriority?: (workflowId: string) => number;
75
+ }
76
+ ```
77
+
78
+ ## WorkflowRegistry
79
+
80
+ The registry maps workflow IDs to workflow definitions:
81
+
82
+ ```typescript
83
+ interface WorkflowRegistry {
84
+ getWorkflow(workflowId: string): Workflow<any, any, any> | null;
85
+ }
86
+
87
+ // Simple implementation
88
+ const registry: WorkflowRegistry = {
89
+ getWorkflow: (id) => {
90
+ const workflows = {
91
+ "document-analysis": documentAnalysisWorkflow,
92
+ "data-processing": dataProcessingWorkflow,
93
+ };
94
+ return workflows[id] ?? null;
95
+ },
96
+ };
97
+
98
+ // With type safety
99
+ const workflowMap: Record<string, Workflow<any, any, any>> = {
100
+ "document-analysis": documentAnalysisWorkflow,
101
+ "data-processing": dataProcessingWorkflow,
102
+ };
103
+
104
+ const registry: WorkflowRegistry = {
105
+ getWorkflow: (id) => workflowMap[id] ?? null,
106
+ };
107
+ ```
108
+
109
+ ## Lifecycle Methods
110
+
111
+ ### start()
112
+
113
+ Start the runtime as a worker that processes jobs and polls for state changes.
114
+
115
+ ```typescript
116
+ await runtime.start();
117
+ // Runtime is now:
118
+ // - Polling for pending workflows
119
+ // - Processing jobs from the queue
120
+ // - Checking suspended stages
121
+ // - Handling graceful shutdown on SIGTERM/SIGINT
122
+ ```
123
+
124
+ ### stop()
125
+
126
+ Stop the runtime gracefully.
127
+
128
+ ```typescript
129
+ runtime.stop();
130
+ // Stops polling and job processing
131
+ // Current job completes before stopping
132
+ ```
133
+
134
+ ## Creating and Running Workflows
135
+
136
+ ### createRun(options)
137
+
138
+ Create a new workflow run. The runtime picks it up automatically on the next poll.
139
+
140
+ ```typescript
141
+ interface CreateRunOptions {
142
+ workflowId: string; // Required
143
+ input: Record<string, unknown>; // Required
144
+ config?: Record<string, unknown>; // Optional
145
+ priority?: number; // Optional (default: 5)
146
+ metadata?: Record<string, unknown>; // Optional domain-specific data
147
+ }
148
+
149
+ const { workflowRunId } = await runtime.createRun({
150
+ workflowId: "document-analysis",
151
+ input: { documentUrl: "https://example.com/doc.pdf" },
152
+ config: {
153
+ extract: { maxLength: 5000 },
154
+ },
155
+ priority: 8, // Higher = more important
156
+ metadata: {
157
+ userId: "user-123",
158
+ requestId: "req-456",
159
+ },
160
+ });
161
+ ```
162
+
163
+ The method:
164
+ 1. Validates the workflow exists in the registry
165
+ 2. Validates input against the workflow's input schema
166
+ 3. Merges provided config with workflow defaults
167
+ 4. Validates merged config against all stage config schemas
168
+ 5. Creates a WorkflowRun record with status PENDING
169
+
170
+ ### transitionWorkflow(workflowRunId)
171
+
172
+ Manually trigger workflow state transition (usually handled automatically).
173
+
174
+ ```typescript
175
+ await runtime.transitionWorkflow(workflowRunId);
176
+ ```
177
+
178
+ ### pollSuspendedStages()
179
+
180
+ Manually check suspended stages (usually handled automatically).
181
+
182
+ ```typescript
183
+ await runtime.pollSuspendedStages();
184
+ ```
185
+
186
+ ## AI Helper Integration
187
+
188
+ ### createAIHelper(topic, logContext?)
189
+
190
+ Create an AIHelper bound to the runtime's logger.
191
+
192
+ ```typescript
193
+ // Simple usage
194
+ const ai = runtime.createAIHelper("my-task");
195
+
196
+ // With log context (for batch operations)
197
+ const logContext = runtime.createLogContext(workflowRunId, stageRecordId);
198
+ const ai = runtime.createAIHelper(`workflow.${workflowRunId}`, logContext);
199
+ ```
200
+
201
+ ### createLogContext(workflowRunId, stageRecordId)
202
+
203
+ Create a log context for AIHelper (enables batch logging to persistence).
204
+
205
+ ```typescript
206
+ const logContext = runtime.createLogContext(workflowRunId, stageRecordId);
207
+ // { workflowRunId, stageRecordId, createLog: fn }
208
+ ```
209
+
210
+ ## Complete Setup Example
211
+
212
+ ```typescript
213
+ import {
214
+ createWorkflowRuntime,
215
+ WorkflowBuilder,
216
+ defineStage,
217
+ } from "@bratsos/workflow-engine";
218
+ import {
219
+ createPrismaWorkflowPersistence,
220
+ createPrismaJobQueue,
221
+ createPrismaAICallLogger,
222
+ } from "@bratsos/workflow-engine/persistence/prisma";
223
+ import { PrismaClient } from "@prisma/client";
224
+ import { z } from "zod";
225
+
226
+ // Initialize Prisma
227
+ const prisma = new PrismaClient();
228
+
229
+ // Define stages
230
+ const helloStage = defineStage({
231
+ id: "hello",
232
+ name: "Hello Stage",
233
+ schemas: {
234
+ input: z.object({ name: z.string() }),
235
+ output: z.object({ greeting: z.string() }),
236
+ config: z.object({ prefix: z.string().default("Hello") }),
237
+ },
238
+ async execute(ctx) {
239
+ return {
240
+ output: { greeting: `${ctx.config.prefix}, ${ctx.input.name}!` },
241
+ };
242
+ },
243
+ });
244
+
245
+ // Build workflow
246
+ const helloWorkflow = new WorkflowBuilder(
247
+ "hello-workflow",
248
+ "Hello Workflow",
249
+ "A simple greeting workflow",
250
+ z.object({ name: z.string() }),
251
+ z.object({ greeting: z.string() })
252
+ )
253
+ .pipe(helloStage)
254
+ .build();
255
+
256
+ // Create registry
257
+ const registry = {
258
+ getWorkflow: (id: string) => {
259
+ if (id === "hello-workflow") return helloWorkflow;
260
+ return null;
261
+ },
262
+ };
263
+
264
+ // Create runtime
265
+ const runtime = createWorkflowRuntime({
266
+ persistence: createPrismaWorkflowPersistence(prisma),
267
+ jobQueue: createPrismaJobQueue(prisma),
268
+ aiCallLogger: createPrismaAICallLogger(prisma),
269
+ registry,
270
+ pollIntervalMs: 5000,
271
+ jobPollIntervalMs: 500,
272
+ });
273
+
274
+ // Start runtime
275
+ async function main() {
276
+ console.log("Starting runtime...");
277
+ await runtime.start();
278
+
279
+ // Create a workflow run
280
+ const { workflowRunId } = await runtime.createRun({
281
+ workflowId: "hello-workflow",
282
+ input: { name: "World" },
283
+ });
284
+
285
+ console.log(`Created workflow run: ${workflowRunId}`);
286
+
287
+ // Runtime will automatically:
288
+ // 1. Pick up the pending workflow
289
+ // 2. Enqueue the first stage
290
+ // 3. Execute the stage
291
+ // 4. Mark workflow as completed
292
+ }
293
+
294
+ main().catch(console.error);
295
+
296
+ // Graceful shutdown
297
+ process.on("SIGTERM", () => {
298
+ runtime.stop();
299
+ prisma.$disconnect();
300
+ });
301
+ ```
302
+
303
+ ## Rerunning Workflows from a Specific Stage
304
+
305
+ You can rerun a workflow starting from a specific stage, skipping earlier stages and using their persisted outputs. This is useful for:
306
+ - Retrying after a stage failure (fix the bug, rerun from the failed stage)
307
+ - Re-processing data with updated stage logic
308
+ - Testing specific stages in isolation
309
+
310
+ ### Using WorkflowExecutor.execute() with fromStage
311
+
312
+ ```typescript
313
+ import { WorkflowExecutor } from "@bratsos/workflow-engine";
314
+
315
+ // Given: A workflow that has already been run (stages 1-4 completed)
316
+ const executor = new WorkflowExecutor(
317
+ workflow,
318
+ workflowRunId,
319
+ workflowType,
320
+ { persistence, aiLogger }
321
+ );
322
+
323
+ // Rerun from stage 3 - skips stages 1-2, runs 3-4
324
+ const result = await executor.execute(
325
+ input, // Original input (not used when fromStage is set)
326
+ config,
327
+ { fromStage: "stage-3" }
328
+ );
329
+ ```
330
+
331
+ ### How It Works
332
+
333
+ 1. **Finds the execution group** containing the specified stage
334
+ 2. **Loads input** from the previous stage's persisted output (or workflow input if first stage)
335
+ 3. **Rebuilds workflowContext** from all completed stages before the target group
336
+ 4. **Deletes stage records** for the target stage and all subsequent stages (clean re-execution)
337
+ 5. **Executes** from the target stage forward
338
+
339
+ ### Requirements
340
+
341
+ - **Previous stages must have been executed** - their outputs must be persisted
342
+ - **Stage must exist** in the workflow definition
343
+
344
+ ### Error Handling
345
+
346
+ ```typescript
347
+ // Error: Stage doesn't exist
348
+ await executor.execute(input, config, { fromStage: "non-existent" });
349
+ // Throws: Stage "non-existent" not found in workflow "my-workflow"
350
+
351
+ // Error: No prior execution
352
+ await executor.execute(input, config, { fromStage: "stage-3" });
353
+ // Throws: Cannot rerun from stage "stage-3": no completed stages found before execution group 3
354
+ ```
355
+
356
+ ### Common Use Cases
357
+
358
+ **Retry After Failure:**
359
+ ```typescript
360
+ // Stage 3 failed, you fixed the bug
361
+ await executor.execute(input, config, { fromStage: "stage-3" });
362
+ ```
363
+
364
+ **Re-process with Updated Logic:**
365
+ ```typescript
366
+ // Updated stage-2 implementation, want to rerun from there
367
+ await executor.execute(input, config, { fromStage: "stage-2" });
368
+ ```
369
+
370
+ **Fresh Start from Beginning:**
371
+ ```typescript
372
+ // Rerun entire workflow
373
+ await executor.execute(input, config, { fromStage: "stage-1" });
374
+ ```
375
+
376
+ ### workflowContext Availability
377
+
378
+ When rerunning from a stage, `ctx.workflowContext` contains outputs from all stages **before** the target group:
379
+
380
+ ```typescript
381
+ // Rerunning from stage-3 (group 3)
382
+ // ctx.workflowContext contains:
383
+ // - "stage-1": { ... } // from group 1
384
+ // - "stage-2": { ... } // from group 2
385
+ // - NOT "stage-3" or later
386
+ ```
387
+
388
+ ## Worker Deployment Patterns
389
+
390
+ ### Single Worker
391
+
392
+ ```typescript
393
+ // worker.ts
394
+ const runtime = createWorkflowRuntime({ ... });
395
+ await runtime.start();
396
+ ```
397
+
398
+ ### Multiple Workers (Horizontal Scaling)
399
+
400
+ ```typescript
401
+ // Each worker gets a unique ID
402
+ const workerId = `worker-${process.env.POD_NAME || process.pid}`;
403
+
404
+ const runtime = createWorkflowRuntime({
405
+ ...config,
406
+ workerId,
407
+ });
408
+
409
+ await runtime.start();
410
+ // Workers compete for jobs using atomic dequeue
411
+ // Each job is processed by exactly one worker
412
+ ```
413
+
414
+ ### API Server + Separate Workers
415
+
416
+ ```typescript
417
+ // api-server.ts - Only creates runs, doesn't process
418
+ const runtime = createWorkflowRuntime({ ...config });
419
+ // Don't call runtime.start()
420
+
421
+ app.post("/workflows/:id/runs", async (req, res) => {
422
+ const { workflowRunId } = await runtime.createRun({
423
+ workflowId: req.params.id,
424
+ input: req.body,
425
+ });
426
+ res.json({ workflowRunId });
427
+ });
428
+
429
+ // worker.ts - Only processes, doesn't create
430
+ const runtime = createWorkflowRuntime({ ...config });
431
+ await runtime.start();
432
+ ```
433
+
434
+ ## Configuration Recommendations
435
+
436
+ ### Development
437
+
438
+ ```typescript
439
+ const runtime = createWorkflowRuntime({
440
+ ...config,
441
+ pollIntervalMs: 2000, // Fast polling for development
442
+ jobPollIntervalMs: 500, // Quick job pickup
443
+ staleJobThresholdMs: 30000, // Short timeout
444
+ });
445
+ ```
446
+
447
+ ### Production
448
+
449
+ ```typescript
450
+ const runtime = createWorkflowRuntime({
451
+ ...config,
452
+ pollIntervalMs: 10000, // Standard polling
453
+ jobPollIntervalMs: 1000, // Balance between latency and DB load
454
+ staleJobThresholdMs: 60000, // Allow for longer processing
455
+ workerId: `worker-${process.env.HOSTNAME}`,
456
+ });
457
+ ```
458
+
459
+ ### High-Throughput
460
+
461
+ ```typescript
462
+ const runtime = createWorkflowRuntime({
463
+ ...config,
464
+ pollIntervalMs: 5000, // More frequent orchestration
465
+ jobPollIntervalMs: 100, // Aggressive job pickup
466
+ staleJobThresholdMs: 120000, // Longer timeout for long jobs
467
+ });
468
+ ```
469
+
470
+ ## Monitoring
471
+
472
+ The runtime logs key events to console:
473
+
474
+ ```
475
+ [Runtime] Starting worker worker-12345-hostname
476
+ [Runtime] Poll interval: 10000ms, Job poll: 1000ms
477
+ [Runtime] Created WorkflowRun abc123 for document-analysis
478
+ [Runtime] Found 1 pending workflows
479
+ [Runtime] Started workflow abc123
480
+ [Runtime] Processing stage extract for workflow abc123
481
+ [Runtime] Worker worker-12345-hostname: processed 10 jobs
482
+ [Runtime] Workflow abc123 completed
483
+ ```
484
+
485
+ For production monitoring, integrate with your observability stack:
486
+
487
+ ```typescript
488
+ // Custom logging
489
+ const originalLog = console.log;
490
+ console.log = (...args) => {
491
+ if (args[0]?.includes("[Runtime]")) {
492
+ metrics.increment("workflow.runtime.log");
493
+ logger.info(args.join(" "));
494
+ }
495
+ originalLog(...args);
496
+ };
497
+ ```