Workflows
A workflow is a recipe for a business process - it defines the steps needed to complete an operation. Workflows in Aiki are durable, versioned, and can contain complex logic.
Defining a Workflow
Workflows are created in two steps:
- Create the workflow definition with a name
- Add versions with implementation logic
import { workflow } from "@aikirun/workflow";
// Step 1: Create workflow definition
const orderWorkflow = workflow({
name: "order-processing",
});
// Step 2: Create a version
const orderWorkflowV1 = orderWorkflow.v("1.0.0", {
async handler(run, input: { orderId: string; amount: number }) {
// Your workflow logic here
const validation = await validateOrder.start(run, input);
const payment = await processPayment.start(run, {
orderId: validation.orderId,
amount: input.amount,
});
return { success: true, orderId: validation.orderId };
},
});Workflow Versioning
Versioning allows safe updates to workflows without breaking existing runs.
const userOnboardingWorkflow = workflow({
name: "user-onboarding",
});
// Version 1.0.0: Simple onboarding
const userOnboardingV1 = userOnboardingWorkflow.v("1.0.0", {
async handler(run, input: { userId: string }) {
await sendWelcomeEmail.start(run, {
userId: input.userId,
});
},
});
// Version 2.0.0: Add profile creation
const userOnboardingV2 = userOnboardingWorkflow.v("2.0.0", {
async handler(run, input: { userId: string }) {
await sendWelcomeEmail.start(run, {
userId: input.userId,
});
await createUserProfile.start(run, {
userId: input.userId,
});
},
});Registering Versions
A worker runs only the versions it registers. List every version you still need to serve in the workflows array. Versions coexist, on the same worker or across separate ones:
const onboardingWorker = worker({
workflows: [userOnboardingV1, userOnboardingV2],
});How Runs Resolve to a Version
- A run is pinned to its version when it is created. A run started against
1.0.0always executes and replays against1.0.0, even after2.0.0is deployed. The version is never reselected mid-run. - Each version has its own queue. A worker subscribes only to the versions in its registry, so a
1.0.0run reaches only workers that register1.0.0. See Subscribers for the queue model. - An unregistered version is never silently upgraded. Stop registering
1.0.0and its in-flight runs are no longer delivered. Aiki never reroutes them to2.0.0. A run that reaches a worker without its version in the registry is rejected, not coerced onto another version.
A version bump is therefore the safe way to ship a breaking change. Publish the new version for new runs, keep the old version registered until its in-flight runs drain, then retire it. Because old runs stay on the handler that created them, the new version's handler can change shape freely.
To change a single version's handler in place instead of publishing a new one, follow the determinism rules. See Refactoring Workflows and Determinism.
Workflow Retry
Configure automatic retries for failed workflows using the retry property:
const orderWorkflowV1 = orderWorkflow.v("1.0.0", {
async handler(run, input: { orderId: string }) {
// Workflow logic...
},
retry: {
type: "exponential",
maxAttempts: 3,
baseDelayMs: 5000,
},
});When a workflow fails (due to an unhandled error or task failure), Aiki will automatically retry it based on your retry strategy. Between retries, the workflow enters an awaiting_retry state.
For detailed guidance on retry strategies, see the Retry Strategies Guide.
Schema Validation
Define schemas to validate workflow input and output:
import { z } from "zod";
const orderWorkflowV1 = orderWorkflow.v("1.0.0", {
schema: {
input: z.object({
orderId: z.string(),
items: z.array(z.string()),
}),
output: z.object({
success: z.boolean(),
total: z.number(),
}),
},
async handler(run, input) {
// ...
return { success: true, total: 100 };
},
});Schemas work with any validation library that implements Standard Schema (Zod, Valibot, ArkType, etc.).
Why use output schemas? For child workflows, cached outputs are validated against the schema. If the cached shape doesn't match, the parent workflow fails immediately. See Refactoring Workflows.
Sharding
Route workflows to specific shards for distributed processing, multi-region deployments, or tenant isolation:
const handle = await orderWorkflowV1
.with()
.opt("shard", "us-east")
.start(client, { orderId: "123" });Workers must be configured to listen to the same shard. A workflow routed to "us-east" will only be picked up by workers with shards: ["us-east"] in their configuration. See Workers for worker-side setup.
Starting Workflows
Execute workflows using the version's .start() method:
const handle = await workflowVersion.start(client, {
userId: "123",
email: "user@example.com",
});
// Access run data
console.log("Started:", handle.run.id);
console.log("Status:", handle.run.state.status);
// Wait for completion
const result = await handle.waitForStatus("completed");
if (result.success) {
console.log("Output:", result.state.output);
} else {
console.log("Failed:", result.cause);
}Use reference IDs for idempotent workflow starts:
const handle = await orderWorkflowV1
.with()
.opt("reference.id", "order-123")
.start(client, { orderId: "123" });With a reference ID, calling start() again with the same input returns the existing run. If the input differs, the default behavior throws an error. Use conflictPolicy: "return_existing" to return the existing run regardless of input differences. See the Reference IDs guide for more details.
Workflow Runs
A workflow run is an instance of a workflow execution. It has:
States
scheduled- Scheduled for future executionqueued- Queued, waiting to be picked up by a workerrunning- Currently executingpaused- Paused by usersleeping- Waiting for a sleep duration to elapseawaiting_event- Waiting for an external eventawaiting_retry- Waiting to retry after failureawaiting_child_workflow- Waiting for a child workflow to completecompleted- Finished successfullyfailed- Encountered a non-retryable error or exhausted all retriescancelled- Cancelled by user
Workflow Handle
A handle is a reference to a workflow run that lets you interact with it from outside the workflow - check its status, wait for completion, send events, or control execution.
The handle returned from .start() provides:
| Property/Method | Description |
|---|---|
run | The workflow run data (id, state, input, output, etc.) |
events | Send events to the workflow |
refresh() | Refresh run data from the server |
waitForStatus(status) | Wait for a terminal status (completed, failed, cancelled) |
cancel(reason?) | Cancel the workflow run |
pause() | Pause the workflow |
resume() | Resume a paused workflow |
awake() | Wake a sleeping workflow |
Waiting for Status
The waitForStatus() method returns a result object:
const result = await handle.waitForStatus("completed");
if (result.success) {
// Workflow reached the requested status
console.log("Output:", result.state.output);
} else {
// Workflow reached a different terminal status
console.log("Ended with:", result.cause);
}Controlling Workflow Execution
// Pause a running workflow
await handle.pause();
// Resume a paused workflow
await handle.resume();
// Cancel a workflow
await handle.cancel("User requested cancellation");Child Workflows
Workflows can start other workflows as children. By default, child workflows run in a fire-and-forget manner - the parent continues without waiting.
Starting a Child Workflow
const parentWorkflowV1 = parentWorkflow.v("1.0.0", {
async handler(run, input) {
// Fire and forget - parent continues immediately
await childWorkflowV1.startAsChild(run, { userId: input.userId });
// Parent continues without waiting for child
await doOtherWork.start(run, input);
},
});Waiting for Child Completion
To wait for a child workflow to complete, call waitForStatus() on the child handle:
const parentWorkflowV1 = parentWorkflow.v("1.0.0", {
async handler(run, input) {
const childHandle = await childWorkflowV1.startAsChild(run, { userId: input.userId });
// Parent suspends until child completes
const result = await childHandle.waitForStatus("completed");
if (result.success) {
return { childOutput: result.state.output };
} else {
throw new Error(`Child failed: ${result.cause}`);
}
},
});You can also wait with a timeout:
const result = await childHandle.waitForStatus("completed", {
timeout: { hours: 1 },
});
if (result.timeout) {
// Child didn't complete within 1 hour
}Like sleeps and events, child workflow waits have an internal queue. On replay, if the parent already waited for a child to complete, the cached result is returned immediately instead of waiting again.
Parallel Child Workflows
Start multiple child workflows and wait for all of them using Promise.all:
const parentWorkflowV1 = parentWorkflow.v("1.0.0", {
async handler(run, input) {
// Start all child workflows
const [userHandle, orderHandle, notifyHandle] = await Promise.all([
processUserV1.startAsChild(run, { userId: input.userId }),
processOrderV1.startAsChild(run, { orderId: input.orderId }),
sendNotificationV1.startAsChild(run, { userId: input.userId }),
]);
// Wait for all to complete
const [userResult, orderResult, notifyResult] = await Promise.all([
userHandle.waitForStatus("completed"),
orderHandle.waitForStatus("completed"),
notifyHandle.waitForStatus("completed"),
]);
return {
user: userResult.state.output,
order: orderResult.state.output,
};
},
});Reference IDs for Child Workflows
Use reference IDs to ensure idempotent child workflow creation:
const childHandle = await childWorkflowV1
.with()
.opt("reference.id", `process-user-${input.userId}`)
.startAsChild(run, { userId: input.userId });Without a reference ID, child workflows are deduplicated by input hash.
Conflict Policies
When starting a child workflow multiple times with the same reference ID but different inputs, Aiki provides two conflict policies:
// Default: "error" - fails the parent workflow
const childHandle = await childWorkflowV1
.with()
.opt("reference.id", "unique-id")
.startAsChild(run, input);
// Alternative: "return_existing" - returns the existing child run
const childHandle = await childWorkflowV1
.with()
.opt("reference", { id: "unique-id", conflictPolicy: "return_existing" })
.startAsChild(run, input);| Policy | Behavior |
|---|---|
"error" (default) | Fails the parent workflow if reference ID exists with different inputs |
"return_existing" | Returns the existing child workflow run |
Next Steps
- Tasks - Learn about task execution
- Workers - Understand worker configuration
- Determinism - Write reliable workflows