@aikirun/workflow 0.8.0 → 0.9.1
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 +19 -213
- package/dist/index.d.ts +1 -0
- package/dist/index.js +21 -28
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @aikirun/workflow
|
|
2
2
|
|
|
3
|
-
Workflow SDK for Aiki durable execution platform
|
|
3
|
+
Workflow SDK for Aiki durable execution platform.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,150 +10,27 @@ npm install @aikirun/workflow
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
-
### Define a Workflow
|
|
14
|
-
|
|
15
13
|
```typescript
|
|
16
14
|
import { workflow } from "@aikirun/workflow";
|
|
17
|
-
import {
|
|
15
|
+
import { sendEmail, createProfile } from "./tasks.ts";
|
|
18
16
|
|
|
17
|
+
// Define a workflow
|
|
19
18
|
export const onboardingWorkflow = workflow({ name: "user-onboarding" });
|
|
20
19
|
|
|
21
20
|
export const onboardingWorkflowV1 = onboardingWorkflow.v("1.0.0", {
|
|
22
21
|
async handler(run, input: { email: string }) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Execute task to mark user as verified
|
|
29
|
-
// (In a real scenario, this would be triggered by an external event)
|
|
30
|
-
await markUserVerified.start(run, { email: input.email });
|
|
31
|
-
|
|
32
|
-
// Sleep for 24 hours before sending tips
|
|
33
|
-
await run.sleep("onboarding-delay", { days: 1 });
|
|
34
|
-
|
|
35
|
-
// Send usage tips
|
|
36
|
-
await sendUsageTips.start(run, { email: input.email });
|
|
37
|
-
|
|
38
|
-
return { success: true, userId: input.email };
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Features
|
|
44
|
-
|
|
45
|
-
- **Durable Execution** - Automatically survives crashes and restarts
|
|
46
|
-
- **Task Orchestration** - Coordinate multiple tasks in sequence
|
|
47
|
-
- **Durable Sleep** - Sleep without consuming resources or blocking workers
|
|
48
|
-
- **State Snapshots** - Automatically save state at each step
|
|
49
|
-
- **Error Handling** - Built-in retry and recovery mechanisms
|
|
50
|
-
- **Multiple Versions** - Run different workflow versions simultaneously
|
|
51
|
-
- **Logging** - Built-in structured logging for debugging
|
|
52
|
-
|
|
53
|
-
## Workflow Primitives
|
|
54
|
-
|
|
55
|
-
### Execute Tasks
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
const result = await createUserProfile.start(run, {
|
|
59
|
-
email: input.email,
|
|
60
|
-
});
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Sleep for a Duration
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
// Sleep requires a unique id for memoization
|
|
67
|
-
await run.sleep("daily-delay", { days: 1 });
|
|
68
|
-
await run.sleep("processing-delay", { hours: 2, minutes: 30 });
|
|
69
|
-
await run.sleep("short-pause", { seconds: 30 });
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Sleep Cancellation
|
|
73
|
-
|
|
74
|
-
Sleeps can be cancelled externally via the `wake()` method:
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
const handle = await myWorkflow.start(client, input);
|
|
78
|
-
await handle.wake(); // Wakes the workflow if sleeping
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
The sleep returns a result indicating whether it was cancelled:
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
const { cancelled } = await run.sleep("wait-period", { hours: 1 });
|
|
85
|
-
if (cancelled) {
|
|
86
|
-
// Handle early wake-up
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Get Workflow State
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
const { state } = await run.handle.getState();
|
|
94
|
-
console.log("Workflow status:", state.status);
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Logging
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
run.logger.info("Processing user", { email: input.email });
|
|
101
|
-
run.logger.debug("User created", { userId: result.userId });
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Workflow Options
|
|
105
|
-
|
|
106
|
-
### Delayed Trigger
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
export const morningWorkflowV1 = morningWorkflow.v("1.0.0", {
|
|
110
|
-
// ... workflow definition
|
|
111
|
-
opts: {
|
|
112
|
-
trigger: {
|
|
113
|
-
type: "delayed",
|
|
114
|
-
delay: { seconds: 5 }, // or: delay: 5000
|
|
115
|
-
},
|
|
22
|
+
await sendEmail.start(run, { email: input.email });
|
|
23
|
+
await run.sleep("welcome-delay", { days: 1 });
|
|
24
|
+
await createProfile.start(run, { email: input.email });
|
|
25
|
+
return { success: true };
|
|
116
26
|
},
|
|
117
27
|
});
|
|
118
28
|
```
|
|
119
29
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
export const paymentWorkflowV1 = paymentWorkflow.v("1.0.0", {
|
|
124
|
-
// ... workflow definition
|
|
125
|
-
opts: {
|
|
126
|
-
retry: {
|
|
127
|
-
type: "exponential",
|
|
128
|
-
maxAttempts: 3,
|
|
129
|
-
baseDelayMs: 1000,
|
|
130
|
-
maxDelayMs: 10000,
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Reference ID
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
// Assign a reference ID for tracking and lookup
|
|
140
|
-
const handle = await orderWorkflowV1
|
|
141
|
-
.with().opt("reference.id", `order-${orderId}`)
|
|
142
|
-
.start(client, { orderId });
|
|
143
|
-
|
|
144
|
-
// Configure conflict handling: "error" (default) or "return_existing"
|
|
145
|
-
const handle = await orderWorkflowV1
|
|
146
|
-
.with().opt("reference", { id: `order-${orderId}`, onConflict: "return_existing" })
|
|
147
|
-
.start(client, { orderId });
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
## Running Workflows
|
|
151
|
-
|
|
152
|
-
With the client:
|
|
30
|
+
Run with a client:
|
|
153
31
|
|
|
154
32
|
```typescript
|
|
155
33
|
import { client } from "@aikirun/client";
|
|
156
|
-
import { onboardingWorkflowV1 } from "./workflows.ts";
|
|
157
34
|
|
|
158
35
|
const aikiClient = await client({
|
|
159
36
|
url: "http://localhost:9876",
|
|
@@ -164,99 +41,28 @@ const handle = await onboardingWorkflowV1.start(aikiClient, {
|
|
|
164
41
|
email: "user@example.com",
|
|
165
42
|
});
|
|
166
43
|
|
|
167
|
-
|
|
168
|
-
const result = await handle.wait(
|
|
169
|
-
{ type: "status", status: "completed" },
|
|
170
|
-
{ maxDurationMs: 60 * 1000, pollIntervalMs: 5_000 },
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
if (result.success) {
|
|
174
|
-
console.log("Workflow completed!", result.state);
|
|
175
|
-
} else {
|
|
176
|
-
console.log("Workflow did not complete:", result.cause);
|
|
177
|
-
}
|
|
44
|
+
const result = await handle.waitForStatus("completed");
|
|
178
45
|
```
|
|
179
46
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
import { worker } from "@aikirun/worker";
|
|
184
|
-
|
|
185
|
-
const aikiWorker = worker({
|
|
186
|
-
name: "my-worker",
|
|
187
|
-
workflows: [onboardingWorkflowV1],
|
|
188
|
-
opts: {
|
|
189
|
-
maxConcurrentWorkflowRuns: 10,
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
await aikiWorker.spawn(aikiClient);
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
## Execution Context
|
|
197
|
-
|
|
198
|
-
The `run` parameter provides access to:
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
interface WorkflowRunContext<Input, Output> {
|
|
202
|
-
id: WorkflowRunId; // Unique run ID
|
|
203
|
-
name: WorkflowName; // Workflow name
|
|
204
|
-
versionId: WorkflowVersionId; // Version ID
|
|
205
|
-
options: WorkflowOptions; // Execution options (trigger, retry, reference)
|
|
206
|
-
handle: WorkflowRunHandle<Input, Output>; // Advanced state management
|
|
207
|
-
logger: Logger; // Logging (info, debug, warn, error, trace)
|
|
208
|
-
sleep(params: SleepParams): Promise<SleepResult>; // Durable sleep
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Sleep parameters:
|
|
213
|
-
- `id` (required): Unique identifier for memoization
|
|
214
|
-
- Duration fields: `days`, `hours`, `minutes`, `seconds`, `milliseconds`
|
|
215
|
-
|
|
216
|
-
Example: `run.sleep("my-sleep", { days: 1, hours: 2 })`
|
|
217
|
-
|
|
218
|
-
## Error Handling
|
|
219
|
-
|
|
220
|
-
Workflows handle errors gracefully:
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
try {
|
|
224
|
-
await risky.start(run, input);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
run.logger.error("Task failed", { error: error.message });
|
|
227
|
-
// Workflow can decide how to proceed
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Failed workflows transition to `awaiting_retry` state and are automatically retried by the server.
|
|
232
|
-
|
|
233
|
-
### Expected Errors
|
|
234
|
-
|
|
235
|
-
These errors are thrown during normal workflow execution and should not be caught in workflow code:
|
|
236
|
-
|
|
237
|
-
- `WorkflowRunSuspendedError` - Thrown when a workflow suspends (e.g., during sleep or awaiting events). The worker catches this error and the workflow resumes when the condition is met.
|
|
47
|
+
## Features
|
|
238
48
|
|
|
239
|
-
-
|
|
49
|
+
- **Durable Execution** - Workflows survive crashes and restarts
|
|
50
|
+
- **Task Orchestration** - Coordinate multiple tasks
|
|
51
|
+
- **Durable Sleep** - Sleep without blocking workers
|
|
52
|
+
- **Event Handling** - Wait for external events with timeouts
|
|
53
|
+
- **Child Workflows** - Compose workflows together
|
|
54
|
+
- **Automatic Retries** - Configurable retry strategies
|
|
55
|
+
- **Versioning** - Run multiple versions simultaneously
|
|
240
56
|
|
|
241
|
-
##
|
|
57
|
+
## Documentation
|
|
242
58
|
|
|
243
|
-
|
|
244
|
-
2. **Expect Replays** - Code may execute multiple times during retries
|
|
245
|
-
3. **Use Descriptive Events** - Name events clearly for debugging
|
|
246
|
-
4. **Handle Timeouts** - Always check `event.received` after waiting
|
|
247
|
-
5. **Log Strategically** - Use logger to track workflow progress
|
|
248
|
-
6. **Version Your Workflows** - Deploy new versions alongside old ones
|
|
59
|
+
For comprehensive documentation including retry strategies, schema validation, child workflows, and best practices, see the [Workflows Guide](https://aiki.run/docs/core-concepts/workflows).
|
|
249
60
|
|
|
250
61
|
## Related Packages
|
|
251
62
|
|
|
252
63
|
- [@aikirun/task](https://www.npmjs.com/package/@aikirun/task) - Define tasks
|
|
253
64
|
- [@aikirun/client](https://www.npmjs.com/package/@aikirun/client) - Start workflows
|
|
254
65
|
- [@aikirun/worker](https://www.npmjs.com/package/@aikirun/worker) - Execute workflows
|
|
255
|
-
- [@aikirun/types](https://www.npmjs.com/package/@aikirun/types) - Type definitions
|
|
256
|
-
|
|
257
|
-
## Changelog
|
|
258
|
-
|
|
259
|
-
See the [CHANGELOG](https://github.com/aikirun/aiki/blob/main/CHANGELOG.md) for version history.
|
|
260
66
|
|
|
261
67
|
## License
|
|
262
68
|
|
package/dist/index.d.ts
CHANGED
|
@@ -281,6 +281,7 @@ declare class WorkflowVersionImpl<Input, Output, AppContext, TEventsDefinition e
|
|
|
281
281
|
private handler;
|
|
282
282
|
private tryExecuteWorkflow;
|
|
283
283
|
private assertRetryAllowed;
|
|
284
|
+
private parse;
|
|
284
285
|
private createFailedState;
|
|
285
286
|
private createAwaitingRetryState;
|
|
286
287
|
}
|
package/dist/index.js
CHANGED
|
@@ -878,19 +878,7 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
|
|
|
878
878
|
parentRunHandle[INTERNAL5].assertExecutionAllowed();
|
|
879
879
|
const { client } = parentRunHandle[INTERNAL5];
|
|
880
880
|
const inputRaw = isNonEmptyArray(args) ? args[0] : void 0;
|
|
881
|
-
|
|
882
|
-
if (this.params.schema?.input) {
|
|
883
|
-
try {
|
|
884
|
-
input = this.params.schema.input.parse(inputRaw);
|
|
885
|
-
} catch (error) {
|
|
886
|
-
await parentRunHandle[INTERNAL5].transitionState({
|
|
887
|
-
status: "failed",
|
|
888
|
-
cause: "self",
|
|
889
|
-
error: createSerializableError(error)
|
|
890
|
-
});
|
|
891
|
-
throw new WorkflowRunFailedError2(parentRun.id, parentRunHandle.run.attempts);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
881
|
+
const input = await this.parse(parentRunHandle, this.params.schema?.input, inputRaw);
|
|
894
882
|
const inputHash = await hashInput(input);
|
|
895
883
|
const reference = this.params.opts?.reference;
|
|
896
884
|
const path = getWorkflowRunPath(this.name, this.versionId, reference?.id ?? inputHash);
|
|
@@ -904,6 +892,9 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
|
|
|
904
892
|
parentRun.logger
|
|
905
893
|
);
|
|
906
894
|
const { run: existingRun } = await client.api.workflowRun.getByIdV1({ id: existingRunInfo.id });
|
|
895
|
+
if (existingRun.state.status === "completed") {
|
|
896
|
+
await this.parse(parentRunHandle, this.params.schema?.output, existingRun.state.output);
|
|
897
|
+
}
|
|
907
898
|
const logger2 = parentRun.logger.child({
|
|
908
899
|
"aiki.childWorkflowName": existingRun.name,
|
|
909
900
|
"aiki.childWorkflowVersionId": existingRun.versionId,
|
|
@@ -985,29 +976,16 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
|
|
|
985
976
|
logger.info("Workflow complete");
|
|
986
977
|
}
|
|
987
978
|
async tryExecuteWorkflow(input, run, context, retryStrategy) {
|
|
979
|
+
const { handle } = run[INTERNAL5];
|
|
988
980
|
while (true) {
|
|
989
981
|
try {
|
|
990
982
|
const outputRaw = await this.params.handler(run, input, context);
|
|
991
|
-
|
|
992
|
-
if (this.params.schema?.output) {
|
|
993
|
-
try {
|
|
994
|
-
output = this.params.schema.output.parse(outputRaw);
|
|
995
|
-
} catch (error) {
|
|
996
|
-
const { handle } = run[INTERNAL5];
|
|
997
|
-
await handle[INTERNAL5].transitionState({
|
|
998
|
-
status: "failed",
|
|
999
|
-
cause: "self",
|
|
1000
|
-
error: createSerializableError(error)
|
|
1001
|
-
});
|
|
1002
|
-
throw new WorkflowRunFailedError2(run.id, handle.run.attempts);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
983
|
+
const output = await this.parse(handle, this.params.schema?.output, outputRaw);
|
|
1005
984
|
return output;
|
|
1006
985
|
} catch (error) {
|
|
1007
986
|
if (error instanceof WorkflowRunSuspendedError4 || error instanceof WorkflowRunFailedError2 || error instanceof WorkflowRunConflictError5) {
|
|
1008
987
|
throw error;
|
|
1009
988
|
}
|
|
1010
|
-
const { handle } = run[INTERNAL5];
|
|
1011
989
|
const attempts = handle.run.attempts;
|
|
1012
990
|
const retryParams = getRetryParams(attempts, retryStrategy);
|
|
1013
991
|
if (!retryParams.retriesLeft) {
|
|
@@ -1052,6 +1030,21 @@ var WorkflowVersionImpl = class _WorkflowVersionImpl {
|
|
|
1052
1030
|
throw error;
|
|
1053
1031
|
}
|
|
1054
1032
|
}
|
|
1033
|
+
async parse(handle, schema, data) {
|
|
1034
|
+
if (!schema) {
|
|
1035
|
+
return data;
|
|
1036
|
+
}
|
|
1037
|
+
try {
|
|
1038
|
+
return schema.parse(data);
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
await handle[INTERNAL5].transitionState({
|
|
1041
|
+
status: "failed",
|
|
1042
|
+
cause: "self",
|
|
1043
|
+
error: createSerializableError(error)
|
|
1044
|
+
});
|
|
1045
|
+
throw new WorkflowRunFailedError2(handle.run.id, handle.run.attempts);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1055
1048
|
createFailedState(error) {
|
|
1056
1049
|
if (error instanceof TaskFailedError) {
|
|
1057
1050
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikirun/workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Workflow SDK for Aiki - define durable workflows with tasks, sleeps, waits, and event handling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"build": "tsup"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@aikirun/types": "0.
|
|
21
|
+
"@aikirun/types": "0.9.1"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|