@donkeylabs/server 0.4.7 → 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/external-jobs.md +420 -0
- package/docs/workflows.md +603 -0
- package/package.json +1 -1
- package/src/core/external-job-socket.ts +356 -0
- package/src/core/external-jobs.ts +237 -0
- package/src/core/index.ts +49 -0
- package/src/core/jobs.ts +652 -9
- package/src/core/workflows.ts +1326 -0
- package/src/core.ts +2 -0
- package/src/harness.ts +3 -0
- package/src/server.ts +15 -2
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
# Workflows
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Use workflows when you need to:
|
|
8
|
+
- Orchestrate multi-step processes (order processing, onboarding, data pipelines)
|
|
9
|
+
- Run steps in parallel and wait for all to complete
|
|
10
|
+
- Make decisions based on previous step outputs
|
|
11
|
+
- Track progress across long-running processes
|
|
12
|
+
- Automatically retry failed steps with backoff
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1. Define a Workflow
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { workflow } from "@donkeylabs/server";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
const orderWorkflow = workflow("process-order")
|
|
23
|
+
// First task: inputSchema validates workflow input
|
|
24
|
+
.task("validate", {
|
|
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
|
+
},
|
|
31
|
+
})
|
|
32
|
+
.choice("check-inventory", {
|
|
33
|
+
choices: [
|
|
34
|
+
{
|
|
35
|
+
condition: (ctx) => ctx.steps.validate.inStock,
|
|
36
|
+
next: "fulfill",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
default: "backorder",
|
|
40
|
+
})
|
|
41
|
+
.parallel("fulfill", {
|
|
42
|
+
branches: [
|
|
43
|
+
workflow.branch("shipping")
|
|
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
|
+
})
|
|
51
|
+
.build(),
|
|
52
|
+
workflow.branch("notification")
|
|
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
|
+
})
|
|
63
|
+
.build(),
|
|
64
|
+
],
|
|
65
|
+
next: "complete",
|
|
66
|
+
})
|
|
67
|
+
// Subsequent tasks: inputSchema as function receives prev step output
|
|
68
|
+
.task("backorder", {
|
|
69
|
+
inputSchema: (prev) => ({ orderId: prev.orderId, total: prev.total }),
|
|
70
|
+
handler: async (input, ctx) => {
|
|
71
|
+
return await ctx.plugins.orders.createBackorder(input);
|
|
72
|
+
},
|
|
73
|
+
next: "complete",
|
|
74
|
+
})
|
|
75
|
+
.pass("complete", { end: true })
|
|
76
|
+
.build();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Register and Start
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Register the workflow
|
|
83
|
+
ctx.core.workflows.register(orderWorkflow);
|
|
84
|
+
|
|
85
|
+
// Start an instance
|
|
86
|
+
const instanceId = await ctx.core.workflows.start("process-order", {
|
|
87
|
+
orderId: "ORD-123",
|
|
88
|
+
customerId: "CUST-456",
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Track Progress
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// Via Events
|
|
96
|
+
ctx.core.events.on("workflow.progress", (data) => {
|
|
97
|
+
console.log(`${data.workflowName}: ${data.progress}%`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Via SSE (client subscribes to workflow:${instanceId})
|
|
101
|
+
ctx.core.events.on("workflow.progress", (data) => {
|
|
102
|
+
ctx.core.sse.broadcast(`workflow:${data.instanceId}`, "progress", data);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Step Types
|
|
107
|
+
|
|
108
|
+
### Task
|
|
109
|
+
|
|
110
|
+
Executes an inline handler function with typed input/output.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
workflow("example")
|
|
114
|
+
.task("step-name", {
|
|
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
|
+
},
|
|
130
|
+
|
|
131
|
+
// Optional: retry configuration
|
|
132
|
+
retry: {
|
|
133
|
+
maxAttempts: 3,
|
|
134
|
+
intervalMs: 1000,
|
|
135
|
+
backoffRate: 2,
|
|
136
|
+
maxIntervalMs: 30000,
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Optional: step timeout in ms
|
|
140
|
+
timeout: 60000,
|
|
141
|
+
|
|
142
|
+
// Control flow (one of these)
|
|
143
|
+
next: "next-step", // Go to specific step
|
|
144
|
+
end: true, // End workflow (mutually exclusive with next)
|
|
145
|
+
})
|
|
146
|
+
```
|
|
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
|
+
|
|
200
|
+
### Parallel
|
|
201
|
+
|
|
202
|
+
Runs multiple workflow branches concurrently.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
workflow("example")
|
|
206
|
+
.parallel("parallel-step", {
|
|
207
|
+
// Required: branches to execute
|
|
208
|
+
branches: [
|
|
209
|
+
workflow.branch("branch-a")
|
|
210
|
+
.task("task-a1", { job: "job-a1" })
|
|
211
|
+
.task("task-a2", { job: "job-a2" })
|
|
212
|
+
.build(),
|
|
213
|
+
|
|
214
|
+
workflow.branch("branch-b")
|
|
215
|
+
.task("task-b1", { job: "job-b1" })
|
|
216
|
+
.build(),
|
|
217
|
+
],
|
|
218
|
+
|
|
219
|
+
// Optional: error handling
|
|
220
|
+
onError: "fail-fast", // Stop all on first error (default)
|
|
221
|
+
// onError: "wait-all", // Wait for all branches, collect errors
|
|
222
|
+
|
|
223
|
+
// Control flow
|
|
224
|
+
next: "next-step",
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The output of a parallel step is an object with each branch's output:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
{
|
|
232
|
+
"branch-a": { /* branch-a output */ },
|
|
233
|
+
"branch-b": { /* branch-b output */ }
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Choice
|
|
238
|
+
|
|
239
|
+
Conditional branching based on workflow context.
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
workflow("example")
|
|
243
|
+
.choice("decision-point", {
|
|
244
|
+
// Evaluated in order, first match wins
|
|
245
|
+
choices: [
|
|
246
|
+
{
|
|
247
|
+
condition: (ctx) => ctx.steps.validate.amount > 1000,
|
|
248
|
+
next: "large-order-flow",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
condition: (ctx) => ctx.steps.validate.isPriority,
|
|
252
|
+
next: "priority-flow",
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
|
|
256
|
+
// Fallback if no conditions match
|
|
257
|
+
default: "standard-flow",
|
|
258
|
+
})
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Pass
|
|
262
|
+
|
|
263
|
+
Transform data or create a no-op step.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
workflow("example")
|
|
267
|
+
// Transform data
|
|
268
|
+
.pass("transform", {
|
|
269
|
+
transform: (ctx) => ({
|
|
270
|
+
summary: {
|
|
271
|
+
input: ctx.input,
|
|
272
|
+
results: ctx.steps,
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
next: "next-step",
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Static result
|
|
279
|
+
.pass("static", {
|
|
280
|
+
result: { status: "initialized" },
|
|
281
|
+
next: "next-step",
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// End marker (shorthand)
|
|
285
|
+
.end("done")
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Workflow Context
|
|
289
|
+
|
|
290
|
+
Every step receives a `WorkflowContext` with:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
interface WorkflowContext {
|
|
294
|
+
/** Original workflow input */
|
|
295
|
+
input: any;
|
|
296
|
+
|
|
297
|
+
/** Results from completed steps (keyed by step name) */
|
|
298
|
+
steps: Record<string, any>;
|
|
299
|
+
|
|
300
|
+
/** Output from the previous step (undefined for first step) */
|
|
301
|
+
prev?: any;
|
|
302
|
+
|
|
303
|
+
/** Current workflow instance */
|
|
304
|
+
instance: WorkflowInstance;
|
|
305
|
+
|
|
306
|
+
/** Type-safe step result getter */
|
|
307
|
+
getStepResult<T>(stepName: string): T | undefined;
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Example usage in step configuration:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// Using inputSchema mapper function (recommended)
|
|
315
|
+
.task("process", {
|
|
316
|
+
inputSchema: (prev, workflowInput) => ({
|
|
317
|
+
orderId: workflowInput.orderId,
|
|
318
|
+
validationResult: prev, // prev = output from previous step
|
|
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
|
+
},
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Retry Configuration
|
|
329
|
+
|
|
330
|
+
Configure retries at the step level or set defaults for the entire workflow:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// Default retry for all steps
|
|
334
|
+
workflow("example")
|
|
335
|
+
.defaultRetry({
|
|
336
|
+
maxAttempts: 3,
|
|
337
|
+
intervalMs: 1000,
|
|
338
|
+
backoffRate: 2,
|
|
339
|
+
maxIntervalMs: 30000,
|
|
340
|
+
})
|
|
341
|
+
.task("step1", { job: "job1" }) // Uses default
|
|
342
|
+
.task("step2", {
|
|
343
|
+
job: "job2",
|
|
344
|
+
retry: { maxAttempts: 5 }, // Override
|
|
345
|
+
})
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Retry parameters:
|
|
349
|
+
- `maxAttempts`: Maximum retry attempts (including first try)
|
|
350
|
+
- `intervalMs`: Initial delay between retries (default: 1000)
|
|
351
|
+
- `backoffRate`: Multiplier for each retry (default: 2)
|
|
352
|
+
- `maxIntervalMs`: Maximum delay cap (default: 30000)
|
|
353
|
+
- `retryOn`: Array of error messages to retry on (default: all errors)
|
|
354
|
+
|
|
355
|
+
## Workflow Timeout
|
|
356
|
+
|
|
357
|
+
Set a timeout for the entire workflow:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
workflow("example")
|
|
361
|
+
.timeout(3600000) // 1 hour max
|
|
362
|
+
.task("step1", { job: "job1" })
|
|
363
|
+
.task("step2", { job: "job2" })
|
|
364
|
+
.build();
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Events
|
|
368
|
+
|
|
369
|
+
Workflows emit events at key points:
|
|
370
|
+
|
|
371
|
+
| Event | Data | Description |
|
|
372
|
+
|-------|------|-------------|
|
|
373
|
+
| `workflow.started` | `{ instanceId, workflowName, input }` | Workflow started |
|
|
374
|
+
| `workflow.progress` | `{ instanceId, workflowName, progress, currentStep, completedSteps, totalSteps }` | Progress update |
|
|
375
|
+
| `workflow.completed` | `{ instanceId, workflowName, output }` | Workflow completed |
|
|
376
|
+
| `workflow.failed` | `{ instanceId, workflowName, error }` | Workflow failed |
|
|
377
|
+
| `workflow.cancelled` | `{ instanceId, workflowName }` | Workflow cancelled |
|
|
378
|
+
| `workflow.step.started` | `{ instanceId, workflowName, stepName, stepType }` | Step started |
|
|
379
|
+
| `workflow.step.completed` | `{ instanceId, workflowName, stepName, output }` | Step completed |
|
|
380
|
+
| `workflow.step.failed` | `{ instanceId, workflowName, stepName, error, attempts }` | Step failed |
|
|
381
|
+
| `workflow.step.retry` | `{ instanceId, workflowName, stepName, attempt, maxAttempts, delay, error }` | Step retrying |
|
|
382
|
+
|
|
383
|
+
### Example: SSE Progress Broadcasting
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// Broadcast all workflow events to SSE
|
|
387
|
+
const workflowEvents = [
|
|
388
|
+
"workflow.progress",
|
|
389
|
+
"workflow.completed",
|
|
390
|
+
"workflow.failed",
|
|
391
|
+
"workflow.step.started",
|
|
392
|
+
"workflow.step.completed",
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
for (const event of workflowEvents) {
|
|
396
|
+
ctx.core.events.on(event, (data) => {
|
|
397
|
+
ctx.core.sse.broadcast(`workflow:${data.instanceId}`, event, data);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## API Reference
|
|
403
|
+
|
|
404
|
+
### Workflows Service
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
interface Workflows {
|
|
408
|
+
/** Register a workflow definition */
|
|
409
|
+
register(definition: WorkflowDefinition): void;
|
|
410
|
+
|
|
411
|
+
/** Start a new workflow instance */
|
|
412
|
+
start<T = any>(workflowName: string, input: T): Promise<string>;
|
|
413
|
+
|
|
414
|
+
/** Get a workflow instance by ID */
|
|
415
|
+
getInstance(instanceId: string): Promise<WorkflowInstance | null>;
|
|
416
|
+
|
|
417
|
+
/** Cancel a running workflow */
|
|
418
|
+
cancel(instanceId: string): Promise<boolean>;
|
|
419
|
+
|
|
420
|
+
/** Get all instances of a workflow */
|
|
421
|
+
getInstances(workflowName: string, status?: WorkflowStatus): Promise<WorkflowInstance[]>;
|
|
422
|
+
|
|
423
|
+
/** Resume workflows after server restart */
|
|
424
|
+
resume(): Promise<void>;
|
|
425
|
+
|
|
426
|
+
/** Stop the workflow service */
|
|
427
|
+
stop(): Promise<void>;
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Workflow Instance
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
interface WorkflowInstance {
|
|
435
|
+
id: string;
|
|
436
|
+
workflowName: string;
|
|
437
|
+
status: "pending" | "running" | "completed" | "failed" | "cancelled" | "timed_out";
|
|
438
|
+
currentStep?: string;
|
|
439
|
+
input: any;
|
|
440
|
+
output?: any;
|
|
441
|
+
error?: string;
|
|
442
|
+
stepResults: Record<string, StepResult>;
|
|
443
|
+
createdAt: Date;
|
|
444
|
+
startedAt?: Date;
|
|
445
|
+
completedAt?: Date;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
interface StepResult {
|
|
449
|
+
stepName: string;
|
|
450
|
+
status: "pending" | "running" | "completed" | "failed" | "skipped";
|
|
451
|
+
input?: any;
|
|
452
|
+
output?: any;
|
|
453
|
+
error?: string;
|
|
454
|
+
startedAt?: Date;
|
|
455
|
+
completedAt?: Date;
|
|
456
|
+
attempts: number;
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Persistence
|
|
461
|
+
|
|
462
|
+
By default, workflows use an in-memory adapter. For production, implement a `WorkflowAdapter`:
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
interface WorkflowAdapter {
|
|
466
|
+
createInstance(instance: Omit<WorkflowInstance, "id">): Promise<WorkflowInstance>;
|
|
467
|
+
getInstance(instanceId: string): Promise<WorkflowInstance | null>;
|
|
468
|
+
updateInstance(instanceId: string, updates: Partial<WorkflowInstance>): Promise<void>;
|
|
469
|
+
deleteInstance(instanceId: string): Promise<boolean>;
|
|
470
|
+
getInstancesByWorkflow(workflowName: string, status?: WorkflowStatus): Promise<WorkflowInstance[]>;
|
|
471
|
+
getRunningInstances(): Promise<WorkflowInstance[]>;
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Configure via `ServerConfig`:
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
const server = new AppServer({
|
|
479
|
+
db: createDatabase(),
|
|
480
|
+
workflows: {
|
|
481
|
+
adapter: new MyDatabaseWorkflowAdapter(db),
|
|
482
|
+
pollInterval: 1000, // How often to check job completion
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Server Restart Resilience
|
|
488
|
+
|
|
489
|
+
Workflows automatically resume after server restart:
|
|
490
|
+
|
|
491
|
+
1. On startup, `workflows.resume()` is called
|
|
492
|
+
2. All instances with `status: "running"` are retrieved
|
|
493
|
+
3. Execution continues from the current step
|
|
494
|
+
|
|
495
|
+
For this to work properly:
|
|
496
|
+
- Use a persistent adapter (not in-memory) in production
|
|
497
|
+
- Jobs should be idempotent when possible
|
|
498
|
+
- The Jobs service must also support restart resilience
|
|
499
|
+
|
|
500
|
+
## Complete Example
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
import { AppServer, workflow, createDatabase } from "@donkeylabs/server";
|
|
504
|
+
import { z } from "zod";
|
|
505
|
+
|
|
506
|
+
// Define workflow with inline handlers
|
|
507
|
+
const onboardingWorkflow = workflow("user-onboarding")
|
|
508
|
+
.timeout(86400000) // 24 hour max
|
|
509
|
+
.defaultRetry({ maxAttempts: 3 })
|
|
510
|
+
|
|
511
|
+
// First step: inputSchema validates workflow input
|
|
512
|
+
.task("create-account", {
|
|
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
|
+
},
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
// Subsequent steps: inputSchema maps previous output
|
|
529
|
+
.task("send-welcome-email", {
|
|
530
|
+
inputSchema: (prev, workflowInput) => ({
|
|
531
|
+
to: workflowInput.email,
|
|
532
|
+
template: "welcome" as const,
|
|
533
|
+
userId: prev.userId,
|
|
534
|
+
}),
|
|
535
|
+
handler: async (input, ctx) => {
|
|
536
|
+
await ctx.plugins.email.send(input);
|
|
537
|
+
return { sent: true };
|
|
538
|
+
},
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
.choice("check-plan", {
|
|
542
|
+
choices: [
|
|
543
|
+
{
|
|
544
|
+
condition: (ctx) => ctx.input.plan === "enterprise",
|
|
545
|
+
next: "enterprise-setup",
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
default: "standard-setup",
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
.task("enterprise-setup", {
|
|
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
|
+
},
|
|
561
|
+
next: "complete",
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
.task("standard-setup", {
|
|
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
|
+
},
|
|
570
|
+
next: "complete",
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
.pass("complete", {
|
|
574
|
+
transform: (ctx) => ({
|
|
575
|
+
userId: ctx.steps["create-account"].userId,
|
|
576
|
+
plan: ctx.input.plan,
|
|
577
|
+
setupComplete: true,
|
|
578
|
+
}),
|
|
579
|
+
end: true,
|
|
580
|
+
})
|
|
581
|
+
.build();
|
|
582
|
+
|
|
583
|
+
// Setup server
|
|
584
|
+
const server = new AppServer({ db: createDatabase() });
|
|
585
|
+
|
|
586
|
+
// Register workflow
|
|
587
|
+
server.getCore().workflows.register(onboardingWorkflow);
|
|
588
|
+
|
|
589
|
+
// Start workflow from a route
|
|
590
|
+
router.route("onboard").typed({
|
|
591
|
+
input: z.object({
|
|
592
|
+
email: z.string().email(),
|
|
593
|
+
name: z.string(),
|
|
594
|
+
plan: z.enum(["free", "pro", "enterprise"]),
|
|
595
|
+
}),
|
|
596
|
+
handle: async (input, ctx) => {
|
|
597
|
+
const instanceId = await ctx.core.workflows.start("user-onboarding", input);
|
|
598
|
+
return { instanceId };
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
await server.start();
|
|
603
|
+
```
|