@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.
package/package.json ADDED
@@ -0,0 +1,112 @@
1
+ {
2
+ "name": "@bratsos/workflow-engine",
3
+ "version": "0.0.11",
4
+ "description": "Type-safe, distributed workflow engine for AI-orchestrated processes with suspend/resume, parallel execution, and cost tracking",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Alex Bratsos",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/bratsos/workflow-engine"
11
+ },
12
+ "homepage": "https://github.com/bratsos/workflow-engine#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/bratsos/workflow-engine/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "workflow",
21
+ "workflow-engine",
22
+ "ai",
23
+ "orchestration",
24
+ "typescript",
25
+ "async",
26
+ "batch-processing",
27
+ "distributed",
28
+ "suspend-resume",
29
+ "llm"
30
+ ],
31
+ "bin": {
32
+ "workflow-engine-sync": "./dist/cli/sync-models.js"
33
+ },
34
+ "main": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "./client": {
43
+ "types": "./dist/client.d.ts",
44
+ "import": "./dist/client.js",
45
+ "default": "./dist/client.js"
46
+ },
47
+ "./testing": {
48
+ "types": "./dist/testing/index.d.ts",
49
+ "import": "./dist/testing/index.js",
50
+ "default": "./dist/testing/index.js"
51
+ },
52
+ "./persistence": {
53
+ "types": "./dist/persistence/index.d.ts",
54
+ "import": "./dist/persistence/index.js",
55
+ "default": "./dist/persistence/index.js"
56
+ },
57
+ "./persistence/prisma": {
58
+ "types": "./dist/persistence/prisma/index.d.ts",
59
+ "import": "./dist/persistence/prisma/index.js",
60
+ "default": "./dist/persistence/prisma/index.js"
61
+ }
62
+ },
63
+ "files": [
64
+ "dist",
65
+ "skills",
66
+ "README.md",
67
+ "LICENSE"
68
+ ],
69
+ "scripts": {
70
+ "typecheck": "tsc --noEmit",
71
+ "build": "tsup",
72
+ "test": "vitest run",
73
+ "sync-models": "tsx src/cli/sync-models.ts"
74
+ },
75
+ "dependencies": {
76
+ "@ai-sdk/google": "^3.0.1",
77
+ "@openrouter/ai-sdk-provider": "^2.1.1",
78
+ "ai": "^6.0.3",
79
+ "js-tiktoken": "^1.0.21",
80
+ "zod": "^4.1.12"
81
+ },
82
+ "peerDependencies": {
83
+ "@anthropic-ai/sdk": ">=0.35.0",
84
+ "@google/genai": ">=1.0.0",
85
+ "@prisma/client": ">=6.0.0",
86
+ "openai": ">=4.0.0"
87
+ },
88
+ "peerDependenciesMeta": {
89
+ "@prisma/client": {
90
+ "optional": true
91
+ },
92
+ "@anthropic-ai/sdk": {
93
+ "optional": true
94
+ },
95
+ "@google/genai": {
96
+ "optional": true
97
+ },
98
+ "openai": {
99
+ "optional": true
100
+ }
101
+ },
102
+ "devDependencies": {
103
+ "@types/node": "^22",
104
+ "tsup": "^8.5.1",
105
+ "tsx": "^4.19.0",
106
+ "typescript": "^5.8.3",
107
+ "vitest": "^3.2.4"
108
+ },
109
+ "engines": {
110
+ "node": ">=22.11.0"
111
+ }
112
+ }
@@ -0,0 +1,539 @@
1
+ ---
2
+ name: workflow-engine
3
+ description: Guide for @bratsos/workflow-engine - a type-safe workflow engine with AI integration, stage pipelines, and persistence. Use when building multi-stage workflows, AI-powered pipelines, implementing workflow persistence, defining stages, or working with batch AI operations.
4
+ license: MIT
5
+ metadata:
6
+ author: bratsos
7
+ version: "0.1.0"
8
+ repository: https://github.com/bratsos/workflow-engine
9
+ ---
10
+
11
+ # @bratsos/workflow-engine Skill
12
+
13
+ Type-safe workflow engine for building AI-powered, multi-stage pipelines with persistence and batch processing support.
14
+
15
+ ## ⚠️ CRITICAL: Required Prisma Models
16
+
17
+ **Before using this library, your Prisma schema MUST include ALL of these models with EXACT field names:**
18
+
19
+ | Model | Required | Purpose |
20
+ |-------|----------|---------|
21
+ | `WorkflowRun` | ✅ Yes | Workflow execution records |
22
+ | `WorkflowStage` | ✅ Yes | Stage execution state |
23
+ | `WorkflowLog` | ✅ Yes | Stage logging |
24
+ | `WorkflowArtifact` | ✅ Yes | Stage output storage |
25
+ | `JobQueue` | ✅ Yes | Job scheduling |
26
+ | `AICall` | Optional | AI call tracking |
27
+
28
+ **Common Errors:**
29
+ - `Cannot read properties of undefined (reading 'create')` → Missing `WorkflowLog` model
30
+ - `Cannot read properties of undefined (reading 'upsert')` → Missing `WorkflowArtifact` model
31
+ - `Unknown argument 'duration'. Did you mean 'durationMs'?` → Field name mismatch (use `duration`, not `durationMs`)
32
+
33
+ **See [05-persistence-setup.md](references/05-persistence-setup.md) for the complete schema.**
34
+
35
+ ## When to Apply
36
+
37
+ - User wants to create workflow stages or pipelines
38
+ - User mentions `defineStage`, `defineAsyncBatchStage`, `WorkflowBuilder`
39
+ - User is implementing workflow persistence with Prisma
40
+ - User needs AI integration (generateText, generateObject, embeddings, batch)
41
+ - User is building multi-stage data processing pipelines
42
+ - User mentions workflow runtime, job queues, or stage execution
43
+ - User wants to rerun a workflow from a specific stage (retry after failure)
44
+ - User needs to test workflows with mocks
45
+
46
+ ## Quick Start
47
+
48
+ ```typescript
49
+ import {
50
+ defineStage,
51
+ WorkflowBuilder,
52
+ createWorkflowRuntime,
53
+ createPrismaWorkflowPersistence,
54
+ createPrismaJobQueue,
55
+ createPrismaAICallLogger,
56
+ } from "@bratsos/workflow-engine";
57
+ import { z } from "zod";
58
+
59
+ // 1. Define a stage
60
+ const processStage = defineStage({
61
+ id: "process",
62
+ name: "Process Data",
63
+ schemas: {
64
+ input: z.object({ data: z.string() }),
65
+ output: z.object({ result: z.string() }),
66
+ config: z.object({ verbose: z.boolean().default(false) }),
67
+ },
68
+ async execute(ctx) {
69
+ const processed = ctx.input.data.toUpperCase();
70
+ return { output: { result: processed } };
71
+ },
72
+ });
73
+
74
+ // 2. Build a workflow
75
+ const workflow = new WorkflowBuilder(
76
+ "my-workflow",
77
+ "My Workflow",
78
+ "Processes data",
79
+ z.object({ data: z.string() }),
80
+ z.object({ result: z.string() })
81
+ )
82
+ .pipe(processStage)
83
+ .build();
84
+
85
+ // 3. Create runtime and execute
86
+ const runtime = createWorkflowRuntime({
87
+ persistence: createPrismaWorkflowPersistence(prisma),
88
+ jobQueue: createPrismaJobQueue(prisma),
89
+ aiCallLogger: createPrismaAICallLogger(prisma),
90
+ registry: { getWorkflow: (id) => (id === "my-workflow" ? workflow : null) },
91
+ });
92
+
93
+ await runtime.start();
94
+ const { workflowRunId } = await runtime.createRun({
95
+ workflowId: "my-workflow",
96
+ input: { data: "hello" },
97
+ });
98
+ ```
99
+
100
+ ## Core Exports Reference
101
+
102
+ | Export | Type | Purpose |
103
+ |--------|------|---------|
104
+ | `defineStage` | Function | Create sync stages |
105
+ | `defineAsyncBatchStage` | Function | Create async/batch stages |
106
+ | `WorkflowBuilder` | Class | Chain stages into workflows |
107
+ | `Workflow` | Class | Built workflow definition |
108
+ | `WorkflowRuntime` | Class | Execute workflows with persistence |
109
+ | `createAIHelper` | Function | AI operations (text, object, embed, batch) |
110
+ | `AVAILABLE_MODELS` | Object | Model configurations |
111
+ | `registerModels` | Function | Add custom models |
112
+ | `calculateCost` | Function | Estimate token costs |
113
+ | `NoInputSchema` | Schema | For stages without input |
114
+
115
+ ## Stage Definition
116
+
117
+ ### Sync Stage
118
+
119
+ ```typescript
120
+ const myStage = defineStage({
121
+ id: "my-stage", // Unique identifier
122
+ name: "My Stage", // Display name
123
+ description: "Optional", // Description
124
+ dependencies: ["prev"], // Required previous stages
125
+
126
+ schemas: {
127
+ input: InputSchema, // Zod schema or "none"
128
+ output: OutputSchema, // Zod schema
129
+ config: ConfigSchema, // Zod schema with defaults
130
+ },
131
+
132
+ async execute(ctx) {
133
+ // Access input, config, workflow context
134
+ const { input, config, workflowContext } = ctx;
135
+
136
+ // Get output from previous stages
137
+ const prevOutput = ctx.require("prev"); // Throws if missing
138
+ const optOutput = ctx.optional("other"); // Returns undefined if missing
139
+
140
+ // Access services
141
+ await ctx.log("INFO", "Processing...");
142
+ await ctx.storage.save("key", data);
143
+
144
+ return {
145
+ output: { ... },
146
+ customMetrics: { itemsProcessed: 10 },
147
+ artifacts: { rawData: data },
148
+ };
149
+ },
150
+ });
151
+ ```
152
+
153
+ ### Async Batch Stage
154
+
155
+ ```typescript
156
+ const batchStage = defineAsyncBatchStage({
157
+ id: "batch-process",
158
+ name: "Batch Process",
159
+ mode: "async-batch", // Required
160
+
161
+ schemas: {
162
+ input: "none",
163
+ output: OutputSchema,
164
+ config: ConfigSchema,
165
+ },
166
+
167
+ async execute(ctx) {
168
+ // Check if resuming from suspension
169
+ if (ctx.resumeState) {
170
+ return { output: ctx.resumeState.cachedResult };
171
+ }
172
+
173
+ // Submit batch and suspend
174
+ const batchId = await submitBatchJob(ctx.input);
175
+
176
+ return {
177
+ suspended: true,
178
+ state: {
179
+ batchId,
180
+ submittedAt: new Date().toISOString(),
181
+ pollInterval: 60000,
182
+ maxWaitTime: 3600000,
183
+ },
184
+ pollConfig: {
185
+ pollInterval: 60000,
186
+ maxWaitTime: 3600000,
187
+ nextPollAt: new Date(Date.now() + 60000),
188
+ },
189
+ };
190
+ },
191
+
192
+ async checkCompletion(suspendedState, ctx) {
193
+ const status = await checkBatchStatus(suspendedState.batchId);
194
+
195
+ if (status === "completed") {
196
+ const results = await getBatchResults(suspendedState.batchId);
197
+ return { ready: true, output: { results } };
198
+ }
199
+
200
+ if (status === "failed") {
201
+ return { ready: false, error: "Batch failed" };
202
+ }
203
+
204
+ return { ready: false, nextCheckIn: 60000 };
205
+ },
206
+ });
207
+ ```
208
+
209
+ ## WorkflowBuilder
210
+
211
+ ```typescript
212
+ const workflow = new WorkflowBuilder(
213
+ "workflow-id",
214
+ "Workflow Name",
215
+ "Description",
216
+ InputSchema,
217
+ OutputSchema
218
+ )
219
+ .pipe(stage1) // Sequential
220
+ .pipe(stage2)
221
+ .parallel([stage3a, stage3b]) // Parallel execution
222
+ .pipe(stage4)
223
+ .build();
224
+
225
+ // Workflow utilities
226
+ workflow.getStageIds(); // ["stage1", "stage2", ...]
227
+ workflow.getExecutionPlan(); // Grouped by execution order
228
+ workflow.getDefaultConfig(); // Default config for all stages
229
+ workflow.validateConfig(config); // Validate config object
230
+ ```
231
+
232
+ ## AI Integration & Cost Tracking
233
+
234
+ ### Topic Convention for Cost Aggregation
235
+
236
+ Use hierarchical topics to enable cost tracking at different levels:
237
+
238
+ ```typescript
239
+ // In a stage - use the standard convention
240
+ const ai = runtime.createAIHelper(`workflow.${ctx.workflowRunId}.stage.${ctx.stageId}`);
241
+
242
+ // Later, query costs by prefix:
243
+ const workflowCost = await aiLogger.getStats(`workflow.${workflowRunId}`); // All stages
244
+ const stageCost = await aiLogger.getStats(`workflow.${workflowRunId}.stage.extraction`); // One stage
245
+ ```
246
+
247
+ **Note:** When a workflow completes, `WorkflowRun.totalCost` and `totalTokens` are automatically populated.
248
+
249
+ ### AI Methods
250
+
251
+ ```typescript
252
+ // Text generation
253
+ const { text, cost } = await ai.generateText("gemini-2.5-flash", prompt, {
254
+ temperature: 0.7,
255
+ maxTokens: 1000,
256
+ });
257
+
258
+ // Structured output
259
+ const { object } = await ai.generateObject(
260
+ "gemini-2.5-flash",
261
+ prompt,
262
+ z.object({ items: z.array(z.string()) })
263
+ );
264
+
265
+ // Embeddings
266
+ const { embedding, embeddings } = await ai.embed(
267
+ "text-embedding-004",
268
+ ["text1", "text2"],
269
+ { dimensions: 768 }
270
+ );
271
+
272
+ // Batch operations (50% cost savings)
273
+ const batch = ai.batch("claude-sonnet-4-20250514", "anthropic");
274
+ const handle = await batch.submit([
275
+ { id: "req1", prompt: "..." },
276
+ { id: "req2", prompt: "...", schema: OutputSchema },
277
+ ]);
278
+
279
+ // Check status and get results
280
+ const status = await batch.getStatus(handle.id);
281
+ const results = await batch.getResults(handle.id);
282
+
283
+ // Get aggregated stats
284
+ const stats = await ai.getStats();
285
+ console.log(`Cost: $${stats.totalCost.toFixed(4)}, Tokens: ${stats.totalInputTokens + stats.totalOutputTokens}`);
286
+ ```
287
+
288
+ **See [04-ai-integration.md](references/04-ai-integration.md) for complete topic convention and cost tracking docs.**
289
+
290
+ ## Persistence Setup
291
+
292
+ ### ⚠️ Required Prisma Models (ALL are required)
293
+
294
+ Copy this complete schema. Missing models or wrong field names will cause runtime errors.
295
+
296
+ ```prisma
297
+ // Required enums
298
+ enum Status {
299
+ PENDING
300
+ RUNNING
301
+ SUSPENDED
302
+ COMPLETED
303
+ FAILED
304
+ CANCELLED
305
+ SKIPPED
306
+ }
307
+
308
+ enum LogLevel {
309
+ DEBUG
310
+ INFO
311
+ WARN
312
+ ERROR
313
+ }
314
+
315
+ enum ArtifactType {
316
+ STAGE_OUTPUT
317
+ ARTIFACT
318
+ METADATA
319
+ }
320
+
321
+ // ✅ REQUIRED: WorkflowRun
322
+ model WorkflowRun {
323
+ id String @id @default(cuid())
324
+ createdAt DateTime @default(now())
325
+ updatedAt DateTime @updatedAt
326
+ workflowId String
327
+ workflowName String
328
+ workflowType String
329
+ status Status @default(PENDING)
330
+ startedAt DateTime?
331
+ completedAt DateTime?
332
+ duration Int? // ⚠️ Must be "duration", not "durationMs"
333
+ input Json
334
+ output Json?
335
+ config Json @default("{}")
336
+ totalCost Float @default(0)
337
+ totalTokens Int @default(0)
338
+ priority Int @default(5)
339
+
340
+ stages WorkflowStage[]
341
+ logs WorkflowLog[]
342
+ artifacts WorkflowArtifact[]
343
+
344
+ @@index([status])
345
+ @@map("workflow_runs")
346
+ }
347
+
348
+ // ✅ REQUIRED: WorkflowStage
349
+ model WorkflowStage {
350
+ id String @id @default(cuid())
351
+ createdAt DateTime @default(now())
352
+ updatedAt DateTime @updatedAt
353
+ workflowRunId String
354
+ stageId String
355
+ stageName String
356
+ stageNumber Int
357
+ executionGroup Int
358
+ status Status @default(PENDING)
359
+ startedAt DateTime?
360
+ completedAt DateTime?
361
+ duration Int? // ⚠️ Must be "duration", not "durationMs"
362
+ inputData Json?
363
+ outputData Json?
364
+ config Json?
365
+ suspendedState Json?
366
+ resumeData Json?
367
+ nextPollAt DateTime?
368
+ pollInterval Int?
369
+ maxWaitUntil DateTime?
370
+ metrics Json?
371
+ embeddingInfo Json?
372
+ errorMessage String?
373
+
374
+ workflowRun WorkflowRun @relation(fields: [workflowRunId], references: [id], onDelete: Cascade)
375
+ logs WorkflowLog[]
376
+ artifacts WorkflowArtifact[]
377
+
378
+ @@unique([workflowRunId, stageId])
379
+ @@map("workflow_stages")
380
+ }
381
+
382
+ // ✅ REQUIRED: WorkflowLog (missing = "Cannot read 'create'" error)
383
+ model WorkflowLog {
384
+ id String @id @default(cuid())
385
+ createdAt DateTime @default(now())
386
+ workflowRunId String?
387
+ workflowStageId String?
388
+ level LogLevel
389
+ message String
390
+ metadata Json?
391
+
392
+ workflowRun WorkflowRun? @relation(fields: [workflowRunId], references: [id], onDelete: Cascade)
393
+ workflowStage WorkflowStage? @relation(fields: [workflowStageId], references: [id], onDelete: Cascade)
394
+
395
+ @@index([workflowRunId])
396
+ @@map("workflow_logs")
397
+ }
398
+
399
+ // ✅ REQUIRED: WorkflowArtifact (missing = "Cannot read 'upsert'" error)
400
+ model WorkflowArtifact {
401
+ id String @id @default(cuid())
402
+ createdAt DateTime @default(now())
403
+ updatedAt DateTime @updatedAt
404
+ workflowRunId String
405
+ workflowStageId String?
406
+ key String
407
+ type ArtifactType
408
+ data Json
409
+ size Int
410
+ metadata Json?
411
+
412
+ workflowRun WorkflowRun @relation(fields: [workflowRunId], references: [id], onDelete: Cascade)
413
+ workflowStage WorkflowStage? @relation(fields: [workflowStageId], references: [id], onDelete: Cascade)
414
+
415
+ @@unique([workflowRunId, key])
416
+ @@map("workflow_artifacts")
417
+ }
418
+
419
+ // ✅ REQUIRED: JobQueue
420
+ model JobQueue {
421
+ id String @id @default(cuid())
422
+ createdAt DateTime @default(now())
423
+ updatedAt DateTime @updatedAt
424
+ workflowRunId String
425
+ stageId String
426
+ status Status @default(PENDING)
427
+ priority Int @default(5)
428
+ workerId String?
429
+ lockedAt DateTime?
430
+ startedAt DateTime?
431
+ completedAt DateTime?
432
+ attempt Int @default(0)
433
+ maxAttempts Int @default(3)
434
+ lastError String?
435
+ nextPollAt DateTime?
436
+ payload Json @default("{}")
437
+
438
+ @@index([status, nextPollAt])
439
+ @@map("job_queue")
440
+ }
441
+ ```
442
+
443
+ ### Create Persistence
444
+
445
+ ```typescript
446
+ import {
447
+ createPrismaWorkflowPersistence,
448
+ createPrismaJobQueue,
449
+ createPrismaAICallLogger,
450
+ } from "@bratsos/workflow-engine/persistence/prisma";
451
+
452
+ // PostgreSQL (default)
453
+ const persistence = createPrismaWorkflowPersistence(prisma);
454
+ const jobQueue = createPrismaJobQueue(prisma);
455
+
456
+ // SQLite - MUST pass databaseType option
457
+ const persistence = createPrismaWorkflowPersistence(prisma, { databaseType: "sqlite" });
458
+ const jobQueue = createPrismaJobQueue(prisma, { databaseType: "sqlite" });
459
+
460
+ const aiCallLogger = createPrismaAICallLogger(prisma);
461
+ ```
462
+
463
+ ## Runtime Configuration
464
+
465
+ ```typescript
466
+ const runtime = createWorkflowRuntime({
467
+ persistence,
468
+ jobQueue,
469
+ aiCallLogger,
470
+ registry: {
471
+ getWorkflow: (id) => workflowMap[id] ?? null,
472
+ },
473
+
474
+ // Optional configuration
475
+ pollIntervalMs: 10000, // Orchestration poll interval
476
+ jobPollIntervalMs: 1000, // Job dequeue interval
477
+ staleJobThresholdMs: 60000, // Stale job timeout
478
+ workerId: "worker-1", // Custom worker ID
479
+ });
480
+
481
+ // Lifecycle
482
+ await runtime.start(); // Start processing
483
+ runtime.stop(); // Graceful shutdown
484
+
485
+ // Manual operations
486
+ await runtime.createRun({ workflowId, input });
487
+ await runtime.transitionWorkflow(runId);
488
+ await runtime.pollSuspendedStages();
489
+ ```
490
+
491
+ ## Testing
492
+
493
+ ```typescript
494
+ import {
495
+ TestWorkflowPersistence,
496
+ TestJobQueue,
497
+ MockAIHelper,
498
+ } from "@bratsos/workflow-engine/testing";
499
+
500
+ const persistence = new TestWorkflowPersistence();
501
+ const jobQueue = new TestJobQueue();
502
+ const mockAI = new MockAIHelper();
503
+
504
+ // Configure mock responses
505
+ mockAI.mockGenerateText("Expected response");
506
+ mockAI.mockGenerateObject({ items: ["a", "b"] });
507
+
508
+ // Test stage execution
509
+ const result = await myStage.execute({
510
+ input: { data: "test" },
511
+ config: { verbose: true },
512
+ workflowContext: {},
513
+ workflowRunId: "test-run",
514
+ stageId: "my-stage",
515
+ log: async () => {},
516
+ storage: persistence.createStorage("test-run"),
517
+ });
518
+ ```
519
+
520
+ ## Reference Files
521
+
522
+ For detailed documentation, see the reference files:
523
+
524
+ - [01-stage-definitions.md](references/01-stage-definitions.md) - Complete stage API
525
+ - [02-workflow-builder.md](references/02-workflow-builder.md) - WorkflowBuilder patterns
526
+ - [03-runtime-setup.md](references/03-runtime-setup.md) - Runtime configuration
527
+ - [04-ai-integration.md](references/04-ai-integration.md) - AI helper methods
528
+ - [05-persistence-setup.md](references/05-persistence-setup.md) - Database setup
529
+ - [06-async-batch-stages.md](references/06-async-batch-stages.md) - Async operations
530
+ - [07-testing-patterns.md](references/07-testing-patterns.md) - Testing utilities
531
+ - [08-common-patterns.md](references/08-common-patterns.md) - Best practices
532
+
533
+ ## Key Principles
534
+
535
+ 1. **Type Safety**: All schemas are Zod - types flow through the entire pipeline
536
+ 2. **Context Access**: Use `ctx.require()` and `ctx.optional()` for type-safe stage output access
537
+ 3. **Unified Status**: Single `Status` enum for workflows, stages, and jobs
538
+ 4. **Cost Tracking**: All AI calls automatically track tokens and costs
539
+ 5. **Batch Savings**: Use async-batch stages for 50% cost savings on large operations