@donkeylabs/server 0.4.8 → 0.5.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.
- package/docs/workflows.md +150 -56
- package/package.json +1 -1
- package/src/core/workflows.ts +202 -49
package/docs/workflows.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Workflows
|
|
2
2
|
|
|
3
|
-
Workflows provide step function / state machine orchestration for complex multi-step processes.
|
|
3
|
+
Workflows provide step function / state machine orchestration for complex multi-step processes. Workflows support sequential tasks with inline handlers, parallel execution, conditional branching, retries, and real-time progress via SSE.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -17,11 +17,17 @@ Use workflows when you need to:
|
|
|
17
17
|
|
|
18
18
|
```typescript
|
|
19
19
|
import { workflow } from "@donkeylabs/server";
|
|
20
|
+
import { z } from "zod";
|
|
20
21
|
|
|
21
22
|
const orderWorkflow = workflow("process-order")
|
|
23
|
+
// First task: inputSchema validates workflow input
|
|
22
24
|
.task("validate", {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
inputSchema: z.object({ orderId: z.string() }),
|
|
26
|
+
outputSchema: z.object({ valid: z.boolean(), inStock: z.boolean(), total: z.number() }),
|
|
27
|
+
handler: async (input, ctx) => {
|
|
28
|
+
const order = await ctx.plugins.orders.validate(input.orderId);
|
|
29
|
+
return { valid: true, inStock: order.inStock, total: order.total };
|
|
30
|
+
},
|
|
25
31
|
})
|
|
26
32
|
.choice("check-inventory", {
|
|
27
33
|
choices: [
|
|
@@ -35,16 +41,35 @@ const orderWorkflow = workflow("process-order")
|
|
|
35
41
|
.parallel("fulfill", {
|
|
36
42
|
branches: [
|
|
37
43
|
workflow.branch("shipping")
|
|
38
|
-
.task("ship", {
|
|
44
|
+
.task("ship", {
|
|
45
|
+
// inputSchema as function: maps previous step output to this step's input
|
|
46
|
+
inputSchema: (prev) => ({ orderId: prev.orderId }),
|
|
47
|
+
handler: async (input, ctx) => {
|
|
48
|
+
return await ctx.plugins.shipping.createShipment(input.orderId);
|
|
49
|
+
},
|
|
50
|
+
})
|
|
39
51
|
.build(),
|
|
40
52
|
workflow.branch("notification")
|
|
41
|
-
.task("notify", {
|
|
53
|
+
.task("notify", {
|
|
54
|
+
inputSchema: (prev, workflowInput) => ({
|
|
55
|
+
orderId: workflowInput.orderId,
|
|
56
|
+
total: prev.total,
|
|
57
|
+
}),
|
|
58
|
+
handler: async (input, ctx) => {
|
|
59
|
+
await ctx.plugins.email.sendConfirmation(input);
|
|
60
|
+
return { sent: true };
|
|
61
|
+
},
|
|
62
|
+
})
|
|
42
63
|
.build(),
|
|
43
64
|
],
|
|
44
65
|
next: "complete",
|
|
45
66
|
})
|
|
67
|
+
// Subsequent tasks: inputSchema as function receives prev step output
|
|
46
68
|
.task("backorder", {
|
|
47
|
-
|
|
69
|
+
inputSchema: (prev) => ({ orderId: prev.orderId, total: prev.total }),
|
|
70
|
+
handler: async (input, ctx) => {
|
|
71
|
+
return await ctx.plugins.orders.createBackorder(input);
|
|
72
|
+
},
|
|
48
73
|
next: "complete",
|
|
49
74
|
})
|
|
50
75
|
.pass("complete", { end: true })
|
|
@@ -82,25 +107,26 @@ ctx.core.events.on("workflow.progress", (data) => {
|
|
|
82
107
|
|
|
83
108
|
### Task
|
|
84
109
|
|
|
85
|
-
Executes
|
|
110
|
+
Executes an inline handler function with typed input/output.
|
|
86
111
|
|
|
87
112
|
```typescript
|
|
88
113
|
workflow("example")
|
|
89
114
|
.task("step-name", {
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}),
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
// Input: Zod schema (for first step) OR mapper function (for subsequent steps)
|
|
116
|
+
// First step - validates workflow input:
|
|
117
|
+
inputSchema: z.object({ orderId: z.string() }),
|
|
118
|
+
// Subsequent steps - maps previous output to this step's input:
|
|
119
|
+
// inputSchema: (prev, workflowInput) => ({ orderId: prev.orderId }),
|
|
120
|
+
|
|
121
|
+
// Optional: Zod schema for output validation
|
|
122
|
+
outputSchema: z.object({ success: z.boolean(), data: z.any() }),
|
|
123
|
+
|
|
124
|
+
// Required: inline handler function
|
|
125
|
+
handler: async (input, ctx) => {
|
|
126
|
+
// input is typed from inputSchema
|
|
127
|
+
// ctx provides access to plugins, prev, steps, etc.
|
|
128
|
+
return { success: true, data: await processOrder(input.orderId) };
|
|
129
|
+
},
|
|
104
130
|
|
|
105
131
|
// Optional: retry configuration
|
|
106
132
|
retry: {
|
|
@@ -119,6 +145,58 @@ workflow("example")
|
|
|
119
145
|
})
|
|
120
146
|
```
|
|
121
147
|
|
|
148
|
+
#### Input Schema Options
|
|
149
|
+
|
|
150
|
+
**Option 1: Zod Schema (first step or when validating workflow input)**
|
|
151
|
+
```typescript
|
|
152
|
+
.task("validate", {
|
|
153
|
+
inputSchema: z.object({ orderId: z.string(), userId: z.string() }),
|
|
154
|
+
handler: async (input, ctx) => {
|
|
155
|
+
// input: { orderId: string, userId: string } - validated from workflow input
|
|
156
|
+
return { valid: true };
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Option 2: Mapper Function (subsequent steps)**
|
|
162
|
+
```typescript
|
|
163
|
+
.task("charge", {
|
|
164
|
+
// prev = output from previous step, workflowInput = original workflow input
|
|
165
|
+
inputSchema: (prev, workflowInput) => ({
|
|
166
|
+
amount: prev.total,
|
|
167
|
+
userId: workflowInput.userId,
|
|
168
|
+
}),
|
|
169
|
+
handler: async (input, ctx) => {
|
|
170
|
+
// input: { amount: number, userId: string } - inferred from mapper return
|
|
171
|
+
return { chargeId: "ch_123" };
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Legacy API (Job-based)
|
|
177
|
+
|
|
178
|
+
For backward compatibility, you can still use job references:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
workflow("example")
|
|
182
|
+
.task("step-name", {
|
|
183
|
+
// Job name to execute
|
|
184
|
+
job: "my-job-name",
|
|
185
|
+
|
|
186
|
+
// Optional: transform workflow context to job input
|
|
187
|
+
input: (ctx) => ({
|
|
188
|
+
orderId: ctx.input.orderId,
|
|
189
|
+
previousResult: ctx.steps.previousStep,
|
|
190
|
+
}),
|
|
191
|
+
|
|
192
|
+
// Optional: transform job result to step output
|
|
193
|
+
output: (result, ctx) => ({
|
|
194
|
+
processed: true,
|
|
195
|
+
data: result.data,
|
|
196
|
+
}),
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
122
200
|
### Parallel
|
|
123
201
|
|
|
124
202
|
Runs multiple workflow branches concurrently.
|
|
@@ -219,6 +297,9 @@ interface WorkflowContext {
|
|
|
219
297
|
/** Results from completed steps (keyed by step name) */
|
|
220
298
|
steps: Record<string, any>;
|
|
221
299
|
|
|
300
|
+
/** Output from the previous step (undefined for first step) */
|
|
301
|
+
prev?: any;
|
|
302
|
+
|
|
222
303
|
/** Current workflow instance */
|
|
223
304
|
instance: WorkflowInstance;
|
|
224
305
|
|
|
@@ -230,18 +311,17 @@ interface WorkflowContext {
|
|
|
230
311
|
Example usage in step configuration:
|
|
231
312
|
|
|
232
313
|
```typescript
|
|
314
|
+
// Using inputSchema mapper function (recommended)
|
|
233
315
|
.task("process", {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
orderId: ctx.input.orderId,
|
|
238
|
-
|
|
239
|
-
// Access previous step output
|
|
240
|
-
validationResult: ctx.steps.validate,
|
|
241
|
-
|
|
242
|
-
// Type-safe access
|
|
243
|
-
amount: ctx.getStepResult<{ amount: number }>("calculate")?.amount,
|
|
316
|
+
inputSchema: (prev, workflowInput) => ({
|
|
317
|
+
orderId: workflowInput.orderId,
|
|
318
|
+
validationResult: prev, // prev = output from previous step
|
|
244
319
|
}),
|
|
320
|
+
handler: async (input, ctx) => {
|
|
321
|
+
// Access any step's output
|
|
322
|
+
const calcResult = ctx.getStepResult<{ amount: number }>("calculate");
|
|
323
|
+
return { processed: true, amount: calcResult?.amount };
|
|
324
|
+
},
|
|
245
325
|
})
|
|
246
326
|
```
|
|
247
327
|
|
|
@@ -421,24 +501,41 @@ For this to work properly:
|
|
|
421
501
|
|
|
422
502
|
```typescript
|
|
423
503
|
import { AppServer, workflow, createDatabase } from "@donkeylabs/server";
|
|
504
|
+
import { z } from "zod";
|
|
424
505
|
|
|
425
|
-
// Define workflow
|
|
506
|
+
// Define workflow with inline handlers
|
|
426
507
|
const onboardingWorkflow = workflow("user-onboarding")
|
|
427
508
|
.timeout(86400000) // 24 hour max
|
|
428
509
|
.defaultRetry({ maxAttempts: 3 })
|
|
429
510
|
|
|
511
|
+
// First step: inputSchema validates workflow input
|
|
430
512
|
.task("create-account", {
|
|
431
|
-
|
|
432
|
-
|
|
513
|
+
inputSchema: z.object({
|
|
514
|
+
email: z.string().email(),
|
|
515
|
+
name: z.string(),
|
|
516
|
+
plan: z.enum(["free", "pro", "enterprise"]),
|
|
517
|
+
}),
|
|
518
|
+
outputSchema: z.object({ userId: z.string() }),
|
|
519
|
+
handler: async (input, ctx) => {
|
|
520
|
+
const user = await ctx.plugins.users.create({
|
|
521
|
+
email: input.email,
|
|
522
|
+
name: input.name,
|
|
523
|
+
});
|
|
524
|
+
return { userId: user.id };
|
|
525
|
+
},
|
|
433
526
|
})
|
|
434
527
|
|
|
528
|
+
// Subsequent steps: inputSchema maps previous output
|
|
435
529
|
.task("send-welcome-email", {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
userId: ctx.steps["create-account"].userId,
|
|
530
|
+
inputSchema: (prev, workflowInput) => ({
|
|
531
|
+
to: workflowInput.email,
|
|
532
|
+
template: "welcome" as const,
|
|
533
|
+
userId: prev.userId,
|
|
441
534
|
}),
|
|
535
|
+
handler: async (input, ctx) => {
|
|
536
|
+
await ctx.plugins.email.send(input);
|
|
537
|
+
return { sent: true };
|
|
538
|
+
},
|
|
442
539
|
})
|
|
443
540
|
|
|
444
541
|
.choice("check-plan", {
|
|
@@ -452,14 +549,24 @@ const onboardingWorkflow = workflow("user-onboarding")
|
|
|
452
549
|
})
|
|
453
550
|
|
|
454
551
|
.task("enterprise-setup", {
|
|
455
|
-
|
|
456
|
-
|
|
552
|
+
// After a choice step, use handler to access specific step outputs
|
|
553
|
+
handler: async (input, ctx) => {
|
|
554
|
+
const userId = ctx.steps["create-account"].userId;
|
|
555
|
+
await ctx.plugins.accounts.setupEnterprise({
|
|
556
|
+
userId,
|
|
557
|
+
features: ["sso", "audit-logs", "dedicated-support"],
|
|
558
|
+
});
|
|
559
|
+
return { setup: "enterprise", userId };
|
|
560
|
+
},
|
|
457
561
|
next: "complete",
|
|
458
562
|
})
|
|
459
563
|
|
|
460
564
|
.task("standard-setup", {
|
|
461
|
-
|
|
462
|
-
|
|
565
|
+
handler: async (input, ctx) => {
|
|
566
|
+
const userId = ctx.steps["create-account"].userId;
|
|
567
|
+
await ctx.plugins.accounts.setupStandard({ userId });
|
|
568
|
+
return { setup: "standard", userId };
|
|
569
|
+
},
|
|
463
570
|
next: "complete",
|
|
464
571
|
})
|
|
465
572
|
|
|
@@ -476,19 +583,6 @@ const onboardingWorkflow = workflow("user-onboarding")
|
|
|
476
583
|
// Setup server
|
|
477
584
|
const server = new AppServer({ db: createDatabase() });
|
|
478
585
|
|
|
479
|
-
// Register jobs that workflows use
|
|
480
|
-
server.getCore().jobs.register("create-user-account", async (data) => {
|
|
481
|
-
// ... create user
|
|
482
|
-
return { userId: "user-123" };
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
server.getCore().jobs.register("send-email", async (data) => {
|
|
486
|
-
// ... send email
|
|
487
|
-
return { sent: true };
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// ... register other jobs
|
|
491
|
-
|
|
492
586
|
// Register workflow
|
|
493
587
|
server.getCore().workflows.register(onboardingWorkflow);
|
|
494
588
|
|
package/package.json
CHANGED
package/src/core/workflows.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Step function / state machine orchestration built on Jobs
|
|
3
3
|
//
|
|
4
4
|
// Supports:
|
|
5
|
-
// - task: Execute
|
|
5
|
+
// - task: Execute inline handler or job (sync or async)
|
|
6
6
|
// - parallel: Run multiple branches concurrently
|
|
7
7
|
// - choice: Conditional branching
|
|
8
8
|
// - pass: Transform data / no-op
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
import type { Events } from "./events";
|
|
11
11
|
import type { Jobs } from "./jobs";
|
|
12
12
|
import type { SSE } from "./sse";
|
|
13
|
+
import type { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// Type helper for Zod schema inference
|
|
16
|
+
type ZodSchema = z.ZodTypeAny;
|
|
17
|
+
type InferZodOutput<T extends ZodSchema> = z.infer<T>;
|
|
13
18
|
|
|
14
19
|
// ============================================
|
|
15
20
|
// Step Types
|
|
@@ -40,14 +45,31 @@ export interface RetryConfig {
|
|
|
40
45
|
retryOn?: string[];
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
// Task Step: Execute
|
|
44
|
-
export interface TaskStepDefinition
|
|
48
|
+
// Task Step: Execute inline handler or job
|
|
49
|
+
export interface TaskStepDefinition<
|
|
50
|
+
TInput extends ZodSchema = ZodSchema,
|
|
51
|
+
TOutput extends ZodSchema = ZodSchema,
|
|
52
|
+
> extends BaseStepDefinition {
|
|
45
53
|
type: "task";
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
/**
|
|
54
|
+
|
|
55
|
+
// === NEW API: Inline handler with Zod schemas ===
|
|
56
|
+
/**
|
|
57
|
+
* Input schema (Zod) OR function that maps previous step output to input.
|
|
58
|
+
* - First task: Use Zod schema, input comes from workflow input
|
|
59
|
+
* - Subsequent tasks: Use function (prev, workflowInput) => inputShape
|
|
60
|
+
*/
|
|
61
|
+
inputSchema?: TInput | ((prev: any, workflowInput: any) => InferZodOutput<TInput>);
|
|
62
|
+
/** Output schema (Zod) for runtime validation and typing */
|
|
63
|
+
outputSchema?: TOutput;
|
|
64
|
+
/** Inline handler function - receives validated input, returns output */
|
|
65
|
+
handler?: (input: InferZodOutput<TInput>, ctx: WorkflowContext) => Promise<InferZodOutput<TOutput>> | InferZodOutput<TOutput>;
|
|
66
|
+
|
|
67
|
+
// === LEGACY API: Job-based execution ===
|
|
68
|
+
/** Job name to execute (legacy - use handler instead) */
|
|
69
|
+
job?: string;
|
|
70
|
+
/** Transform workflow context to job input (legacy) */
|
|
49
71
|
input?: (ctx: WorkflowContext) => any;
|
|
50
|
-
/** Transform job result to step output */
|
|
72
|
+
/** Transform job result to step output (legacy) */
|
|
51
73
|
output?: (result: any, ctx: WorkflowContext) => any;
|
|
52
74
|
}
|
|
53
75
|
|
|
@@ -164,6 +186,8 @@ export interface WorkflowContext {
|
|
|
164
186
|
input: any;
|
|
165
187
|
/** Results from completed steps */
|
|
166
188
|
steps: Record<string, any>;
|
|
189
|
+
/** Output from the previous step (undefined for first step) */
|
|
190
|
+
prev?: any;
|
|
167
191
|
/** Current workflow instance */
|
|
168
192
|
instance: WorkflowInstance;
|
|
169
193
|
/** Get a step result with type safety */
|
|
@@ -270,29 +294,76 @@ export class WorkflowBuilder {
|
|
|
270
294
|
return this;
|
|
271
295
|
}
|
|
272
296
|
|
|
273
|
-
/**
|
|
274
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Add a task step with inline handler (recommended) or job reference.
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* // New API with inline handler and Zod schemas
|
|
302
|
+
* .task("validate", {
|
|
303
|
+
* inputSchema: z.object({ orderId: z.string() }),
|
|
304
|
+
* outputSchema: z.object({ valid: z.boolean(), total: z.number() }),
|
|
305
|
+
* handler: async (input, ctx) => {
|
|
306
|
+
* return { valid: true, total: 99.99 };
|
|
307
|
+
* },
|
|
308
|
+
* })
|
|
309
|
+
*
|
|
310
|
+
* // Using input mapper from previous step
|
|
311
|
+
* .task("charge", {
|
|
312
|
+
* inputSchema: (prev) => ({ amount: prev.total }),
|
|
313
|
+
* outputSchema: z.object({ chargeId: z.string() }),
|
|
314
|
+
* handler: async (input, ctx) => {
|
|
315
|
+
* return { chargeId: "ch_123" };
|
|
316
|
+
* },
|
|
317
|
+
* })
|
|
318
|
+
*
|
|
319
|
+
* // Legacy API (still supported)
|
|
320
|
+
* .task("process", { job: "process-order" })
|
|
321
|
+
*/
|
|
322
|
+
task<TInput extends ZodSchema = ZodSchema, TOutput extends ZodSchema = ZodSchema>(
|
|
275
323
|
name: string,
|
|
276
|
-
config:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
324
|
+
config:
|
|
325
|
+
| {
|
|
326
|
+
// New API: Inline handler with typed schemas
|
|
327
|
+
inputSchema?: TInput | ((prev: any, workflowInput: any) => InferZodOutput<TInput>);
|
|
328
|
+
outputSchema?: TOutput;
|
|
329
|
+
handler: (
|
|
330
|
+
input: InferZodOutput<TInput>,
|
|
331
|
+
ctx: WorkflowContext
|
|
332
|
+
) => Promise<InferZodOutput<TOutput>> | InferZodOutput<TOutput>;
|
|
333
|
+
retry?: RetryConfig;
|
|
334
|
+
timeout?: number;
|
|
335
|
+
next?: string;
|
|
336
|
+
end?: boolean;
|
|
337
|
+
}
|
|
338
|
+
| {
|
|
339
|
+
// Legacy API: Job reference
|
|
340
|
+
job: string;
|
|
341
|
+
input?: (ctx: WorkflowContext) => any;
|
|
342
|
+
output?: (result: any, ctx: WorkflowContext) => any;
|
|
343
|
+
retry?: RetryConfig;
|
|
344
|
+
timeout?: number;
|
|
345
|
+
next?: string;
|
|
346
|
+
end?: boolean;
|
|
347
|
+
}
|
|
285
348
|
): this {
|
|
286
|
-
|
|
349
|
+
// Determine which API is being used
|
|
350
|
+
const isNewApi = "handler" in config;
|
|
351
|
+
|
|
352
|
+
const step: TaskStepDefinition<TInput, TOutput> = {
|
|
287
353
|
name,
|
|
288
354
|
type: "task",
|
|
289
|
-
job: config.job,
|
|
290
|
-
input: config.input,
|
|
291
|
-
output: config.output,
|
|
292
355
|
retry: config.retry,
|
|
293
356
|
timeout: config.timeout,
|
|
294
357
|
next: config.next,
|
|
295
358
|
end: config.end,
|
|
359
|
+
// New API fields
|
|
360
|
+
inputSchema: isNewApi ? (config as any).inputSchema : undefined,
|
|
361
|
+
outputSchema: isNewApi ? (config as any).outputSchema : undefined,
|
|
362
|
+
handler: isNewApi ? (config as any).handler : undefined,
|
|
363
|
+
// Legacy API fields
|
|
364
|
+
job: !isNewApi ? (config as any).job : undefined,
|
|
365
|
+
input: !isNewApi ? (config as any).input : undefined,
|
|
366
|
+
output: !isNewApi ? (config as any).output : undefined,
|
|
296
367
|
};
|
|
297
368
|
|
|
298
369
|
this.addStep(step);
|
|
@@ -623,7 +694,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
623
694
|
}
|
|
624
695
|
|
|
625
696
|
// Build context
|
|
626
|
-
const ctx = this.buildContext(instance);
|
|
697
|
+
const ctx = this.buildContext(instance, definition);
|
|
627
698
|
|
|
628
699
|
// Emit step started event
|
|
629
700
|
await this.emitEvent("workflow.step.started", {
|
|
@@ -676,37 +747,93 @@ class WorkflowsImpl implements Workflows {
|
|
|
676
747
|
ctx: WorkflowContext,
|
|
677
748
|
definition: WorkflowDefinition
|
|
678
749
|
): Promise<any> {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
750
|
+
// Determine which API is being used
|
|
751
|
+
const useInlineHandler = !!step.handler;
|
|
682
752
|
|
|
683
|
-
|
|
684
|
-
|
|
753
|
+
if (useInlineHandler) {
|
|
754
|
+
// === NEW API: Inline handler with Zod schemas ===
|
|
755
|
+
let input: any;
|
|
685
756
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
757
|
+
if (step.inputSchema) {
|
|
758
|
+
if (typeof step.inputSchema === "function") {
|
|
759
|
+
// inputSchema is a mapper function: (prev, workflowInput) => input
|
|
760
|
+
input = step.inputSchema(ctx.prev, ctx.input);
|
|
761
|
+
} else {
|
|
762
|
+
// inputSchema is a Zod schema - validate workflow input
|
|
763
|
+
const parseResult = step.inputSchema.safeParse(ctx.input);
|
|
764
|
+
if (!parseResult.success) {
|
|
765
|
+
throw new Error(`Input validation failed: ${parseResult.error.message}`);
|
|
766
|
+
}
|
|
767
|
+
input = parseResult.data;
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
// No input schema, use workflow input directly
|
|
771
|
+
input = ctx.input;
|
|
695
772
|
}
|
|
696
|
-
}
|
|
697
773
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
774
|
+
// Update step with input
|
|
775
|
+
const instance = await this.adapter.getInstance(instanceId);
|
|
776
|
+
if (instance) {
|
|
777
|
+
const stepResult = instance.stepResults[step.name];
|
|
778
|
+
if (stepResult) {
|
|
779
|
+
stepResult.input = input;
|
|
780
|
+
await this.adapter.updateInstance(instanceId, {
|
|
781
|
+
stepResults: { ...instance.stepResults, [step.name]: stepResult },
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Execute the inline handler
|
|
787
|
+
let result = await step.handler!(input, ctx);
|
|
704
788
|
|
|
705
|
-
|
|
706
|
-
|
|
789
|
+
// Validate output if schema provided
|
|
790
|
+
if (step.outputSchema) {
|
|
791
|
+
const parseResult = step.outputSchema.safeParse(result);
|
|
792
|
+
if (!parseResult.success) {
|
|
793
|
+
throw new Error(`Output validation failed: ${parseResult.error.message}`);
|
|
794
|
+
}
|
|
795
|
+
result = parseResult.data;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return result;
|
|
799
|
+
} else {
|
|
800
|
+
// === LEGACY API: Job-based execution ===
|
|
801
|
+
if (!this.jobs) {
|
|
802
|
+
throw new Error("Jobs service not configured");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (!step.job) {
|
|
806
|
+
throw new Error("Task step requires either 'handler' or 'job'");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Prepare job input
|
|
810
|
+
const jobInput = step.input ? step.input(ctx) : ctx.input;
|
|
811
|
+
|
|
812
|
+
// Update step with input
|
|
813
|
+
const instance = await this.adapter.getInstance(instanceId);
|
|
814
|
+
if (instance) {
|
|
815
|
+
const stepResult = instance.stepResults[step.name];
|
|
816
|
+
if (stepResult) {
|
|
817
|
+
stepResult.input = jobInput;
|
|
818
|
+
await this.adapter.updateInstance(instanceId, {
|
|
819
|
+
stepResults: { ...instance.stepResults, [step.name]: stepResult },
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Enqueue the job
|
|
825
|
+
const jobId = await this.jobs.enqueue(step.job, {
|
|
826
|
+
...jobInput,
|
|
827
|
+
_workflowInstanceId: instanceId,
|
|
828
|
+
_workflowStepName: step.name,
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// Wait for job completion
|
|
832
|
+
const result = await this.waitForJob(jobId, step.timeout);
|
|
707
833
|
|
|
708
|
-
|
|
709
|
-
|
|
834
|
+
// Transform output if needed
|
|
835
|
+
return step.output ? step.output(result, ctx) : result;
|
|
836
|
+
}
|
|
710
837
|
}
|
|
711
838
|
|
|
712
839
|
private async waitForJob(jobId: string, timeout?: number): Promise<any> {
|
|
@@ -930,7 +1057,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
930
1057
|
return ctx.input;
|
|
931
1058
|
}
|
|
932
1059
|
|
|
933
|
-
private buildContext(instance: WorkflowInstance): WorkflowContext {
|
|
1060
|
+
private buildContext(instance: WorkflowInstance, definition: WorkflowDefinition): WorkflowContext {
|
|
934
1061
|
// Build steps object with outputs
|
|
935
1062
|
const steps: Record<string, any> = {};
|
|
936
1063
|
for (const [name, result] of Object.entries(instance.stepResults)) {
|
|
@@ -939,9 +1066,35 @@ class WorkflowsImpl implements Workflows {
|
|
|
939
1066
|
}
|
|
940
1067
|
}
|
|
941
1068
|
|
|
1069
|
+
// Find the previous step's output by tracing the workflow path
|
|
1070
|
+
let prev: any = undefined;
|
|
1071
|
+
if (instance.currentStep) {
|
|
1072
|
+
// Find which step comes before current step
|
|
1073
|
+
for (const [stepName, stepDef] of definition.steps) {
|
|
1074
|
+
if (stepDef.next === instance.currentStep && steps[stepName] !== undefined) {
|
|
1075
|
+
prev = steps[stepName];
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
// If no explicit next found, use most recent completed step output
|
|
1080
|
+
if (prev === undefined) {
|
|
1081
|
+
const completedSteps = Object.entries(instance.stepResults)
|
|
1082
|
+
.filter(([, r]) => r.status === "completed" && r.output !== undefined)
|
|
1083
|
+
.sort((a, b) => {
|
|
1084
|
+
const aTime = a[1].completedAt?.getTime() ?? 0;
|
|
1085
|
+
const bTime = b[1].completedAt?.getTime() ?? 0;
|
|
1086
|
+
return bTime - aTime;
|
|
1087
|
+
});
|
|
1088
|
+
if (completedSteps.length > 0) {
|
|
1089
|
+
prev = completedSteps[0][1].output;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
942
1094
|
return {
|
|
943
1095
|
input: instance.input,
|
|
944
1096
|
steps,
|
|
1097
|
+
prev,
|
|
945
1098
|
instance,
|
|
946
1099
|
getStepResult: <T = any>(stepName: string): T | undefined => {
|
|
947
1100
|
return steps[stepName] as T | undefined;
|