@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @aikirun/workflow
2
2
 
3
- Workflow SDK for Aiki durable execution platform - define durable workflows with tasks, sleeps, waits, and event handling.
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 { markUserVerified, sendVerificationEmail } from "./tasks.ts";
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
- run.logger.info("Starting onboarding", { email: input.email });
24
-
25
- // Execute a task to send verification email
26
- await sendVerificationEmail.start(run, { email: input.email });
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
- ### Retry Strategy
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
- // Wait for completion
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
- With a worker:
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
- - `WorkflowRunConflictError` - Thrown when another worker has already claimed the workflow execution. This prevents duplicate execution when workers race to process the same workflow.
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
- ## Best Practices
57
+ ## Documentation
242
58
 
243
- 1. **Keep Workflows Deterministic** - Same input should always produce same output
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
- let input = inputRaw;
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
- let output = outputRaw;
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.8.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.8.0"
21
+ "@aikirun/types": "0.9.1"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"