Aiki
Core Concepts

Workers

Long-lived processes that execute your workflows in your infrastructure and share the load.

A worker executes your workflows. It runs in your infrastructure, subscribes to workflow run messages, and executes the workflow logic you've defined. You can run multiple workers to scale horizontally—they automatically share the workload.

Creating a Worker

import { client } from "@aikirun/client";
import { worker } from "@aikirun/worker";
import { orderWorkflowV1, userWorkflowV1 } from "./workflows";

const aikiClient = client({
  url: "http://localhost:9850",
  apiKey: "your-api-key",
});

const aikiWorker = worker({
  workflows: [orderWorkflowV1, userWorkflowV1],
  options: {
    maxConcurrentWorkflowRuns: 10,
  },
});

const handle = aikiWorker.start(aikiClient);

Worker definitions are static and reusable. The worker() function creates a definition with a workflows array specifying which workflow versions it can execute. Call start(client) to begin execution; it returns a handle for controlling the running worker.

How Workers Operate

When you call start(), the worker begins discovering ready workflow runs through its subscriber — claiming them from the server by default. When a workflow run is triggered, the worker picks it up, looks up the workflow definition in its registry, and begins execution.

During execution, the worker periodically refreshes its claim on the run. This prevents other workers from thinking it's stuck. If a worker crashes mid-execution, the claim expires after a configurable idle time (default: 90 seconds) and the run is handed to a healthy worker. The workflow then re-executes from its last checkpoint. Workflow Run Claims covers this ownership and recovery in full.

When execution completes or fails, the worker reports the terminal state to the server.

Scaling

Workers scale naturally. You can add capacity in several ways:

Run multiple instances of the same worker to share load. Each gets a portion of the work automatically:

const worker1 = worker({ workflows: [orderWorkflowV1] });
const worker2 = worker({ workflows: [orderWorkflowV1] });

const handle1 = worker1.start(aikiClient);
const handle2 = worker2.start(aikiClient);

Specialize workers by registering different workflows on different workers. Each worker only handles the workflows it knows about.

Split fleets by capability, tenant, or region using pools. A worker with pools: ["tenant-acme"] only processes workflow runs routed to that pool.

Graceful Shutdown

Always handle shutdown signals to let active workflows complete:

process.on("SIGTERM", async () => {
  await handle.stop();
  process.exit(0);
});

The stop() method on the handle signals the worker to stop accepting new work, waits for active executions to finish (up to gracefulShutdownTimeoutMs), then returns. Any workflows that don't complete in time keep their state in the database; once their claims go stale, the server recovers them and other workers pick them up.

Configuration Reference

Worker configuration is split between params (identity) and options (tuning).

Params are passed directly to worker():

ParamDescription
workflowsWorkflow versions this worker executes
subscriberOptional subscriber factory for work discovery (default: claims from the server over HTTP). Use inMemoryQueue() from @aikirun/memory or redisSubscriber() from @aikirun/redis for push delivery

Options are passed via options param or with() builder:

OptionDefaultDescription
maxConcurrentWorkflowRuns1Max parallel executions
gracefulShutdownTimeoutMs5,000Shutdown wait time (ms)
workflowRun.claimRefreshIntervalMs30,000How often the worker refreshes its run claim (ms)
poolsWorker pools to process

Pluggable Subscribers

By default, workers claim work from the server over HTTP, which requires no setup beyond the Aiki server connection. Two subscribers replace that polling with sub-second push delivery. Which one you want follows from where the worker runs.

When the worker runs in the same process as the server, inMemoryQueue() pairs a publisher and a subscriber over one in-process broker, with no external service:

npm install @aikirun/memory
import { inMemoryQueue } from "@aikirun/memory";

const queue = inMemoryQueue();

const aikiServer = server({
  db: database({ provider: "pg", url: databaseUrl }),
  runtime: { publisher: queue.publisher },
});

const aikiWorker = worker({
  workflows: [orderWorkflowV1],
  subscriber: queue.subscriber,
});

When the server and workers are separate processes, the broker has to be too. @aikirun/redis delivers the same way across instances:

npm install @aikirun/redis
import { redisSubscriber } from "@aikirun/redis";

const aikiWorker = worker({
  workflows: [orderWorkflowV1],
  subscriber: redisSubscriber({ url: "redis://localhost:6379" }),
});

Both subscribers pair with a publisher on the server: work reaches a worker only if the server is configured to publish to the same broker. See Server.

You can also implement your own subscriber by providing a function that matches the CreateSubscriber type from @aikirun/types/infra/queue. See Subscribers for how the implementations work and what custom subscribers must provide.

Next Steps

On this page