@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,503 @@
1
+ # Common Patterns
2
+
3
+ Best practices, recipes, and troubleshooting for the workflow engine.
4
+
5
+ ## Document Processing Pipeline
6
+
7
+ A common pattern for processing documents through multiple stages:
8
+
9
+ ```typescript
10
+ import { WorkflowBuilder, defineStage } from "@bratsos/workflow-engine";
11
+ import { z } from "zod";
12
+
13
+ // Stage 1: Extract content
14
+ const extractStage = defineStage({
15
+ id: "extract",
16
+ name: "Extract Content",
17
+ schemas: {
18
+ input: z.object({ documentUrl: z.string().url() }),
19
+ output: z.object({
20
+ text: z.string(),
21
+ metadata: z.object({
22
+ title: z.string().optional(),
23
+ pageCount: z.number(),
24
+ }),
25
+ }),
26
+ config: z.object({
27
+ maxPages: z.number().default(100),
28
+ }),
29
+ },
30
+ async execute(ctx) {
31
+ const content = await fetchAndExtract(ctx.input.documentUrl, ctx.config);
32
+ return { output: content };
33
+ },
34
+ });
35
+
36
+ // Stage 2: Analyze (parallel)
37
+ const classifyStage = defineStage({
38
+ id: "classify",
39
+ name: "Classify",
40
+ dependencies: ["extract"],
41
+ schemas: {
42
+ input: "none",
43
+ output: z.object({ categories: z.array(z.string()) }),
44
+ config: z.object({ model: z.string().default("gemini-2.5-flash") }),
45
+ },
46
+ async execute(ctx) {
47
+ const { text } = ctx.require("extract");
48
+ const ai = createAIHelper("classify", aiLogger);
49
+ const { object } = await ai.generateObject(ctx.config.model, text, ctx.schemas.output);
50
+ return { output: object };
51
+ },
52
+ });
53
+
54
+ const summarizeStage = defineStage({
55
+ id: "summarize",
56
+ name: "Summarize",
57
+ dependencies: ["extract"],
58
+ schemas: {
59
+ input: "none",
60
+ output: z.object({ summary: z.string() }),
61
+ config: z.object({
62
+ maxWords: z.number().default(200),
63
+ model: z.string().default("gemini-2.5-flash"),
64
+ }),
65
+ },
66
+ async execute(ctx) {
67
+ const { text } = ctx.require("extract");
68
+ const ai = createAIHelper("summarize", aiLogger);
69
+ const { text: summary } = await ai.generateText(
70
+ ctx.config.model,
71
+ `Summarize in ${ctx.config.maxWords} words:\n\n${text}`
72
+ );
73
+ return { output: { summary } };
74
+ },
75
+ });
76
+
77
+ // Stage 3: Merge results
78
+ const mergeStage = defineStage({
79
+ id: "merge",
80
+ name: "Merge Results",
81
+ dependencies: ["classify", "summarize"],
82
+ schemas: {
83
+ input: "none",
84
+ output: z.object({
85
+ title: z.string().optional(),
86
+ summary: z.string(),
87
+ categories: z.array(z.string()),
88
+ }),
89
+ config: z.object({}),
90
+ },
91
+ async execute(ctx) {
92
+ const extraction = ctx.require("extract");
93
+ const classification = ctx.require("classify");
94
+ const summary = ctx.require("summarize");
95
+
96
+ return {
97
+ output: {
98
+ title: extraction.metadata.title,
99
+ summary: summary.summary,
100
+ categories: classification.categories,
101
+ },
102
+ };
103
+ },
104
+ });
105
+
106
+ // Build workflow
107
+ const documentPipeline = new WorkflowBuilder(
108
+ "document-pipeline",
109
+ "Document Pipeline",
110
+ "Extract, classify, and summarize documents",
111
+ z.object({ documentUrl: z.string().url() }),
112
+ z.object({ title: z.string().optional(), summary: z.string(), categories: z.array(z.string()) })
113
+ )
114
+ .pipe(extractStage)
115
+ .parallel([classifyStage, summarizeStage])
116
+ .pipe(mergeStage)
117
+ .build();
118
+ ```
119
+
120
+ ## AI Classification Workflow
121
+
122
+ Pattern for multi-label classification with confidence scores:
123
+
124
+ ```typescript
125
+ const ClassificationSchema = z.object({
126
+ labels: z.array(z.object({
127
+ name: z.string(),
128
+ confidence: z.number().min(0).max(1),
129
+ reasoning: z.string(),
130
+ })),
131
+ primaryLabel: z.string(),
132
+ });
133
+
134
+ const classificationStage = defineStage({
135
+ id: "classification",
136
+ name: "AI Classification",
137
+ schemas: {
138
+ input: z.object({ text: z.string() }),
139
+ output: ClassificationSchema,
140
+ config: z.object({
141
+ model: z.string().default("gemini-2.5-flash"),
142
+ labels: z.array(z.string()),
143
+ minConfidence: z.number().default(0.7),
144
+ }),
145
+ },
146
+ async execute(ctx) {
147
+ const ai = createAIHelper("classification", aiLogger);
148
+
149
+ const prompt = `Classify the following text into these categories: ${ctx.config.labels.join(", ")}
150
+
151
+ For each applicable label, provide:
152
+ - The label name
153
+ - A confidence score (0-1)
154
+ - Brief reasoning
155
+
156
+ Text to classify:
157
+ ${ctx.input.text}`;
158
+
159
+ const { object } = await ai.generateObject(ctx.config.model, prompt, ClassificationSchema);
160
+
161
+ // Filter by minimum confidence
162
+ const filtered = {
163
+ ...object,
164
+ labels: object.labels.filter(l => l.confidence >= ctx.config.minConfidence),
165
+ };
166
+
167
+ return { output: filtered };
168
+ },
169
+ });
170
+ ```
171
+
172
+ ## Error Recovery Pattern
173
+
174
+ Graceful error handling with fallbacks:
175
+
176
+ ```typescript
177
+ const robustStage = defineStage({
178
+ id: "robust-stage",
179
+ name: "Robust Stage",
180
+ schemas: {
181
+ input: z.object({ data: z.any() }),
182
+ output: z.object({
183
+ result: z.any(),
184
+ usedFallback: z.boolean(),
185
+ error: z.string().optional(),
186
+ }),
187
+ config: z.object({
188
+ primaryModel: z.string().default("claude-sonnet-4-20250514"),
189
+ fallbackModel: z.string().default("gemini-2.5-flash"),
190
+ maxRetries: z.number().default(3),
191
+ }),
192
+ },
193
+ async execute(ctx) {
194
+ const ai = createAIHelper("robust", aiLogger);
195
+
196
+ // Try primary model
197
+ for (let attempt = 1; attempt <= ctx.config.maxRetries; attempt++) {
198
+ try {
199
+ const result = await ai.generateText(ctx.config.primaryModel, ctx.input.data);
200
+ return {
201
+ output: { result: result.text, usedFallback: false },
202
+ };
203
+ } catch (error) {
204
+ await ctx.log("WARN", `Primary model failed (attempt ${attempt})`, {
205
+ error: error.message,
206
+ });
207
+
208
+ if (attempt === ctx.config.maxRetries) break;
209
+ await sleep(1000 * attempt); // Exponential backoff
210
+ }
211
+ }
212
+
213
+ // Try fallback model
214
+ try {
215
+ await ctx.log("INFO", "Using fallback model");
216
+ const result = await ai.generateText(ctx.config.fallbackModel, ctx.input.data);
217
+ return {
218
+ output: {
219
+ result: result.text,
220
+ usedFallback: true,
221
+ error: "Primary model failed, used fallback",
222
+ },
223
+ };
224
+ } catch (error) {
225
+ await ctx.log("ERROR", "Fallback model also failed");
226
+ throw new Error(`All models failed: ${error.message}`);
227
+ }
228
+ },
229
+ });
230
+ ```
231
+
232
+ ## Cost Optimization Strategies
233
+
234
+ ### 1. Use Batch Operations
235
+
236
+ ```typescript
237
+ // Instead of many individual calls
238
+ for (const item of items) {
239
+ await ai.generateText(model, item.prompt); // Expensive!
240
+ }
241
+
242
+ // Use batch for 50% savings
243
+ const batch = ai.batch(model, "anthropic");
244
+ const handle = await batch.submit(items.map((item, i) => ({
245
+ id: `item-${i}`,
246
+ prompt: item.prompt,
247
+ })));
248
+ ```
249
+
250
+ ### 2. Cache Expensive Results
251
+
252
+ ```typescript
253
+ async execute(ctx) {
254
+ const cacheKey = `result-${hash(ctx.input)}`;
255
+
256
+ // Check cache
257
+ if (await ctx.storage.exists(cacheKey)) {
258
+ return { output: await ctx.storage.load(cacheKey) };
259
+ }
260
+
261
+ // Compute expensive result
262
+ const result = await expensiveOperation(ctx.input);
263
+
264
+ // Cache for future runs
265
+ await ctx.storage.save(cacheKey, result);
266
+
267
+ return { output: result };
268
+ }
269
+ ```
270
+
271
+ ### 3. Use Appropriate Models
272
+
273
+ ```typescript
274
+ // Quick classification - use fast model
275
+ const { object } = await ai.generateObject("gemini-2.5-flash", prompt, schema);
276
+
277
+ // Complex reasoning - use powerful model
278
+ const { text } = await ai.generateText("claude-sonnet-4-20250514", complexPrompt, {
279
+ maxTokens: 4000,
280
+ });
281
+ ```
282
+
283
+ ### 4. Optimize Token Usage
284
+
285
+ ```typescript
286
+ // Truncate long inputs
287
+ const truncatedText = text.slice(0, 50000);
288
+
289
+ // Use structured output to reduce tokens
290
+ const { object } = await ai.generateObject(model, prompt, schema);
291
+ // vs: const { text } = await ai.generateText(model, prompt); JSON.parse(text);
292
+ ```
293
+
294
+ ## Logging Best Practices
295
+
296
+ ### Structured Logging
297
+
298
+ ```typescript
299
+ async execute(ctx) {
300
+ await ctx.log("INFO", "Stage started", {
301
+ inputSize: JSON.stringify(ctx.input).length,
302
+ config: ctx.config,
303
+ });
304
+
305
+ try {
306
+ const result = await processData(ctx.input);
307
+
308
+ await ctx.log("INFO", "Processing complete", {
309
+ itemsProcessed: result.items.length,
310
+ duration: Date.now() - startTime,
311
+ });
312
+
313
+ return { output: result };
314
+ } catch (error) {
315
+ await ctx.log("ERROR", "Processing failed", {
316
+ error: error.message,
317
+ stack: error.stack,
318
+ });
319
+ throw error;
320
+ }
321
+ }
322
+ ```
323
+
324
+ ### Log Levels
325
+
326
+ | Level | Use For |
327
+ |-------|---------|
328
+ | DEBUG | Detailed debugging info |
329
+ | INFO | Normal operations |
330
+ | WARN | Recoverable issues |
331
+ | ERROR | Failures |
332
+
333
+ ## Type-Safe Context Passing
334
+
335
+ ### Define Workflow Context Type
336
+
337
+ ```typescript
338
+ // Define exact shape of workflow context
339
+ type MyWorkflowContext = {
340
+ "extract": { text: string; metadata: { title: string } };
341
+ "classify": { categories: string[] };
342
+ "summarize": { summary: string };
343
+ };
344
+
345
+ // Use in stage definition
346
+ const mergeStage = defineStage<
347
+ "none",
348
+ typeof OutputSchema,
349
+ typeof ConfigSchema,
350
+ MyWorkflowContext
351
+ >({
352
+ id: "merge",
353
+ // ctx.require("extract") is now typed as { text: string; metadata: { title: string } }
354
+ async execute(ctx) {
355
+ const extract = ctx.require("extract"); // Typed!
356
+ const classify = ctx.require("classify"); // Typed!
357
+ },
358
+ });
359
+ ```
360
+
361
+ ### Infer from Workflow
362
+
363
+ ```typescript
364
+ import type { InferWorkflowContext } from "@bratsos/workflow-engine";
365
+
366
+ const workflow = new WorkflowBuilder(...)
367
+ .pipe(extractStage)
368
+ .pipe(classifyStage)
369
+ .build();
370
+
371
+ type WorkflowContext = InferWorkflowContext<typeof workflow>;
372
+ ```
373
+
374
+ ## Troubleshooting Guide
375
+
376
+ ### Stage Not Found in Context
377
+
378
+ **Error**: `Missing required stage output: "stage-id"`
379
+
380
+ **Cause**: Stage dependency not in workflow or hasn't run yet.
381
+
382
+ **Fix**:
383
+ 1. Check `dependencies` array includes the stage
384
+ 2. Verify stage is added before dependent stage in workflow
385
+ 3. Use `ctx.optional()` if stage is truly optional
386
+
387
+ ### Batch Never Completes
388
+
389
+ **Symptoms**: Stage stays SUSPENDED forever
390
+
391
+ **Causes**:
392
+ 1. `nextPollAt` not being updated
393
+ 2. `maxWaitTime` too short
394
+ 3. Batch provider issues
395
+
396
+ **Fix**:
397
+ ```typescript
398
+ // In checkCompletion
399
+ return {
400
+ ready: false,
401
+ nextCheckIn: 60000, // Make sure this is set
402
+ };
403
+
404
+ // Check batch status manually
405
+ const batch = ai.batch(model, provider);
406
+ const status = await batch.getStatus(batchId);
407
+ console.log("Batch status:", status);
408
+ ```
409
+
410
+ ### Type Mismatch in Workflow
411
+
412
+ **Error**: TypeScript errors about incompatible types
413
+
414
+ **Cause**: Output of one stage doesn't match input of next.
415
+
416
+ **Fix**:
417
+ 1. Use `input: "none"` for stages that only use context
418
+ 2. Ensure schemas match across stages
419
+ 3. Use `.passthrough()` on Zod schemas for flexibility
420
+
421
+ ### Prisma Enum Errors
422
+
423
+ **Error**: `Invalid value for argument 'status'. Expected Status`
424
+
425
+ **Cause**: Prisma 7.x requires typed enums, not strings
426
+
427
+ **Fix**: The library handles this automatically via the enum compatibility layer. If you see this error:
428
+ 1. Ensure you're using `createPrismaWorkflowPersistence(prisma)`
429
+ 2. Check you're not bypassing the persistence layer with direct Prisma calls
430
+
431
+ ### Memory Issues with Large Batches
432
+
433
+ **Symptoms**: Process crashes or slows down
434
+
435
+ **Fix**:
436
+ ```typescript
437
+ // Process in smaller batches
438
+ const CHUNK_SIZE = 100;
439
+ for (let i = 0; i < items.length; i += CHUNK_SIZE) {
440
+ const chunk = items.slice(i, i + CHUNK_SIZE);
441
+ await processChunk(chunk);
442
+ }
443
+
444
+ // Stream instead of loading all at once
445
+ for await (const chunk of streamResults(batchId)) {
446
+ yield processChunk(chunk);
447
+ }
448
+ ```
449
+
450
+ ### Workflow Stuck in RUNNING
451
+
452
+ **Symptoms**: Workflow never completes
453
+
454
+ **Causes**:
455
+ 1. Stage threw unhandled error
456
+ 2. Job queue not processing
457
+ 3. Worker crashed
458
+
459
+ **Fix**:
460
+ ```typescript
461
+ // Check for failed stages
462
+ const stages = await persistence.getStagesByRun(runId);
463
+ const failed = stages.filter(s => s.status === "FAILED");
464
+ if (failed.length > 0) {
465
+ console.log("Failed stages:", failed.map(s => ({ id: s.stageId, error: s.errorMessage })));
466
+ }
467
+
468
+ // Release stale jobs
469
+ await jobQueue.releaseStaleJobs(60000);
470
+ ```
471
+
472
+ ## Configuration Recipes
473
+
474
+ ### Development Config
475
+
476
+ ```typescript
477
+ const devConfig = {
478
+ pollIntervalMs: 2000,
479
+ jobPollIntervalMs: 500,
480
+ staleJobThresholdMs: 30000,
481
+ };
482
+ ```
483
+
484
+ ### Production Config
485
+
486
+ ```typescript
487
+ const prodConfig = {
488
+ pollIntervalMs: 10000,
489
+ jobPollIntervalMs: 1000,
490
+ staleJobThresholdMs: 120000,
491
+ workerId: `worker-${process.env.HOSTNAME}`,
492
+ };
493
+ ```
494
+
495
+ ### High-Throughput Config
496
+
497
+ ```typescript
498
+ const throughputConfig = {
499
+ pollIntervalMs: 5000,
500
+ jobPollIntervalMs: 100,
501
+ staleJobThresholdMs: 300000,
502
+ };
503
+ ```