Aiki
Core Concepts

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:

  1. Create the workflow definition with a name
  2. 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.0 always executes and replays against 1.0.0, even after 2.0.0 is 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.0 run reaches only workers that register 1.0.0. See Subscribers for the queue model.
  • An unregistered version is never silently upgraded. Stop registering 1.0.0 and its in-flight runs are no longer delivered. Aiki never reroutes them to 2.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 execution
  • queued - Queued, waiting to be picked up by a worker
  • running - Currently executing
  • paused - Paused by user
  • sleeping - Waiting for a sleep duration to elapse
  • awaiting_event - Waiting for an external event
  • awaiting_retry - Waiting to retry after failure
  • awaiting_child_workflow - Waiting for a child workflow to complete
  • completed - Finished successfully
  • failed - Encountered a non-retryable error or exhausted all retries
  • cancelled - 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/MethodDescription
runThe workflow run data (id, state, input, output, etc.)
eventsSend 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);
PolicyBehavior
"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

On this page