@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Workflows
2
2
 
3
- Workflows provide step function / state machine orchestration for complex multi-step processes. Built on top of the Jobs service, workflows support sequential tasks, parallel execution, conditional branching, retries, and real-time progress via SSE.
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
- job: "validate-order",
24
- input: (ctx) => ({ orderId: ctx.input.orderId }),
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", { job: "create-shipment" })
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", { job: "send-confirmation" })
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
- job: "create-backorder",
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 a job (in-process or external) and waits for completion.
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
- // Required: job to execute
91
- job: "my-job-name",
92
-
93
- // Optional: transform workflow context to job input
94
- input: (ctx) => ({
95
- orderId: ctx.input.orderId,
96
- previousResult: ctx.steps.previousStep,
97
- }),
98
-
99
- // Optional: transform job result to step output
100
- output: (result, ctx) => ({
101
- processed: true,
102
- data: result.data,
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
- job: "process-data",
235
- input: (ctx) => ({
236
- // Access original input
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
- job: "create-user-account",
432
- input: (ctx) => ctx.input,
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
- job: "send-email",
437
- input: (ctx) => ({
438
- to: ctx.input.email,
439
- template: "welcome",
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
- job: "setup-enterprise",
456
- input: (ctx) => ({ userId: ctx.steps["create-account"].userId }),
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
- job: "setup-standard",
462
- input: (ctx) => ({ userId: ctx.steps["create-account"].userId }),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -2,7 +2,7 @@
2
2
  // Step function / state machine orchestration built on Jobs
3
3
  //
4
4
  // Supports:
5
- // - task: Execute a job (sync or async)
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 a job
44
- export interface TaskStepDefinition extends BaseStepDefinition {
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
- /** Job name to execute */
47
- job: string;
48
- /** Transform workflow context to job input */
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
- /** Add a task step that executes a job */
274
- task(
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
- job: string;
278
- input?: (ctx: WorkflowContext) => any;
279
- output?: (result: any, ctx: WorkflowContext) => any;
280
- retry?: RetryConfig;
281
- timeout?: number;
282
- next?: string;
283
- end?: boolean;
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
- const step: TaskStepDefinition = {
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
- if (!this.jobs) {
680
- throw new Error("Jobs service not configured");
681
- }
750
+ // Determine which API is being used
751
+ const useInlineHandler = !!step.handler;
682
752
 
683
- // Prepare job input
684
- const jobInput = step.input ? step.input(ctx) : ctx.input;
753
+ if (useInlineHandler) {
754
+ // === NEW API: Inline handler with Zod schemas ===
755
+ let input: any;
685
756
 
686
- // Update step with input
687
- const instance = await this.adapter.getInstance(instanceId);
688
- if (instance) {
689
- const stepResult = instance.stepResults[step.name];
690
- if (stepResult) {
691
- stepResult.input = jobInput;
692
- await this.adapter.updateInstance(instanceId, {
693
- stepResults: { ...instance.stepResults, [step.name]: stepResult },
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
- // Enqueue the job
699
- const jobId = await this.jobs.enqueue(step.job, {
700
- ...jobInput,
701
- _workflowInstanceId: instanceId,
702
- _workflowStepName: step.name,
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
- // Wait for job completion
706
- const result = await this.waitForJob(jobId, step.timeout);
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
- // Transform output if needed
709
- return step.output ? step.output(result, ctx) : result;
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;