@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/README.md +986 -0
- package/package.json +112 -0
- package/skills/workflow-engine/SKILL.md +539 -0
- package/skills/workflow-engine/references/01-stage-definitions.md +535 -0
- package/skills/workflow-engine/references/02-workflow-builder.md +421 -0
- package/skills/workflow-engine/references/03-runtime-setup.md +497 -0
- package/skills/workflow-engine/references/04-ai-integration.md +587 -0
- package/skills/workflow-engine/references/05-persistence-setup.md +614 -0
- package/skills/workflow-engine/references/06-async-batch-stages.md +514 -0
- package/skills/workflow-engine/references/07-testing-patterns.md +546 -0
- package/skills/workflow-engine/references/08-common-patterns.md +503 -0
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
|