Aiki
Architecture

Server

The orchestrator, shipped as a library: an HTTP handler you mount, plus background daemons.

The Aiki server coordinates workflow execution — and it's a library. server({ db }) returns two pieces:

  • handler — a fetch-style HTTP handler (Request) => Promise<Response> serving the RPC API that clients call. Mount it in any HTTP framework.
  • runtime — background daemons that drive workflow state transitions. Start it with runtime.start().

The bundled standalone server (app/server) is a thin composition of this same library — see Installation.

Request Handling

The RPC API that clients call to:

  • Create workflow runs — validate input, persist state, queue for execution
  • Update workflow state — process state transitions from workers
  • Update task state — record task results and failures
  • Send events — deliver events to waiting workflows
  • Claim ready runs — atomically hand ready work to workers
  • Query runs — list and filter workflow runs

Work Distribution

When a workflow run becomes ready, the server records it in an outbox — the database table that is the source of truth for deliverable work. From there:

  • Default — workers claim pending runs through the server's claim API. No infrastructure beyond the database.
  • With a publisher configured (e.g. @aikirun/redis) — the runtime also pushes ready runs to per-workflow queues the moment they're due, for sub-second delivery. The outbox remains the recovery path: anything lost in transit is re-published.
  • Endpoints (push, coming soon) — the server sends a signed HTTP request to your endpoint handler; no subscriber involved.

See Subscribers for the worker side of this.

Background Daemons

The runtime's daemons drive workflow state transitions:

DaemonPurpose
Scheduled runsQueue scheduled workflow runs when their start time arrives
Sleep elapsedWake sleeping workflows whose sleep duration has elapsed
Workflow retriesRe-queue workflows in awaiting_retry when their retry delay expires
Task retriesRe-queue workflows whose tasks are awaiting retry
Event wait timeoutsResume workflows that timed out waiting for events
Child wait timeoutsResume workflows that timed out waiting for child workflows
Recurring schedulesCreate new runs for cron and interval schedules
Publish pending outbox entriesPublish pending outbox entries to the work queue
Recover overdue outbox entriesRelease abandoned claims and re-pool published entries whose republish time arrived
Stall undeliverable runsGive up on runs undelivered past the retention cap, moving them to stalled (see Stalled Runs)
Due-timers consumerFire near-term timers from the timer priority queue (when configured)

The publish daemon runs only when a publisher is configured; without one, workers claim work directly from the outbox. Recovery runs unconditionally — a crashed worker's claim is released after claimIdleTimeoutMs either way (see Workflow Run Claims).

By default, due work is detected by periodic database scans, so pickup latency is the scan interval. Configuring a timer priority queue (@aikirun/memory in-process, @aikirun/redis shared) promotes near-term timers into a sorted queue that fires them with sub-second precision — embedded servers should configure at least the in-process one. The request path feeds the queue too: creating a run — or waking a parked one — enqueues its timer immediately when the run is due soon, so a run meant to start now doesn't wait for the next scan. The bundled standalone server always runs one: in-process by default, Redis-backed when REDIS_URL is set. With multiple server instances, prefer Redis — the shared queue hands each timer to exactly one instance, where per-instance in-process queues wake every instance for every timer (correctness is not affected, but work is duplicated). The queue is disposable acceleration state — the database keeps every deadline, and the scans repopulate the queue after a restart.

Configuration

Embedded, the server is configured by composition:

import { inMemoryTimerPriorityQueue } from "@aikirun/memory";
import { database, server } from "@aikirun/server";

const aikiServer = server({
  db: database({ provider: "pg", url: databaseUrl }),
  timerPriorityQueue: inMemoryTimerPriorityQueue(),
});

const runtimeHandle = aikiServer.runtime.start();

Optional pieces plug in the same way — cache, iam (multi-tenancy and auth), and the Redis-backed adapters:

import { redisPublisher, redisTimerPriorityQueue } from "@aikirun/redis";
import { Redis } from "ioredis";

const redis = new Redis("redis://localhost:6379");

const aikiServer = server({
  db: database({ provider: "pg", url: databaseUrl }),
  timerPriorityQueue: redisTimerPriorityQueue(redis, "aiki:timers"),
  runtime: {
    publisher: redisPublisher(redis),
  },
});

For the bundled standalone server's environment variables, see the Installation Guide.

Next Steps

On this page