Runlane
Configuration

Runlane CLI

Run maintenance, workers, and operator actions from a small command line surface.

@runlane/cli is the operational command surface for a runtime you already own. It does not discover tasks, lanes, storage, or transport by itself; a config module exports the runtime instance or a factory, and every runtime-backed command calls public runtime APIs such as runlane.trigger(), runlane.tick(), runlane.worker(), and runlane.runs.*.

pnpm add @runlane/cli @runlane/core @runlane/lane-local

Config Module

The packaged runlane binary loads runlane.config.ts, runlane.config.mts, runlane.config.mjs, runlane.config.js, or runlane.config.cjs from the current working directory. Pass --config when the file lives somewhere else.

Keep this file as a thin CLI adapter. Do not duplicate lane construction, task registration, or environment defaults here if the application already owns them. Put the real runtime construction in one shared module and have the CLI config import that module.

For example, the application can own a shared runtime factory:

import { createRunlane, queue } from '@runlane/core'
import { createLocalLane } from '@runlane/lane-local'

import { sendEmail } from './tasks/send-email.js'

const emailQueue = queue({ name: 'emails', default: true })

export function createAppRunlane() {
  return createRunlane({
    lane: createLocalLane(),
    queues: [emailQueue],
    tasks: { sendEmail },
  })
}

Then runlane.config.ts stays boring:

import { type RunlaneCliConfig } from '@runlane/cli'

import { createAppRunlane } from './runtime/runlane.ts'

export default {
  runtime: createAppRunlane,
} satisfies RunlaneCliConfig

TypeScript config files are loaded through tsx, so local development does not require a precompile step. Plain JavaScript config files still load through Node's normal module loader.

The exported runtime value may be a runtime object or an async factory. The CLI starts the runtime before each runtime-backed command and closes it afterward. The runtime must expose start(), close(), tick(), trigger(), worker(), queues, and runs.cancel(), runs.events(), runs.get(), runs.list(), runs.prune(), runs.rerun(), and runs.retry(). The trigger command also needs createRunlane({ tasks }) in the configured runtime so it can resolve the task id passed on the command line.

If you want the CLI inside an existing app script instead of a config file, call runRunlaneCli({ loadRuntime }) directly:

import { runRunlaneCli } from '@runlane/cli'

import { runlane } from './runtime.js'

const result = await runRunlaneCli({
  argv: process.argv.slice(2),
  loadRuntime: () => runlane,
})

process.exitCode = result.exitCode

Use --json before the command to emit machine-readable output:

runlane --json runs list --status failed --limit 20
SwitchBehavior
-c, --config <path>Loads a config module from a non-default path for runtime-backed commands. Relative paths resolve from the current working directory.
--jsonEmits command results and Runlane errors as JSON objects. Long-running lifecycle output is newline-delimited JSON. Command output goes to stdout; errors go to stderr.

CLI commands normally run in the current Node process. When runlane dev runs against a runtime whose storage reports processLocalState: true, it opens a loopback control server and writes a config-keyed session file under the OS temp directory. Stateful operator commands such as runlane trigger, runlane tick, runlane runs list, runlane runs get, runlane retry, runlane rerun, runlane cancel, and runlane prune check for that matching dev session before loading their own runtime; when the session exists, the request is proxied into the live runlane dev process. This is what makes in-memory local lanes work across two terminals.

Shared-process lanes do not need that bridge. If storage state is reachable from another process, the command loads the configured runtime directly and calls the same public APIs in the current process.

The control server is a local development bridge, not a transport adapter. It asks the dev runtime to call runlane.trigger(), and core still owns run creation, outbox persistence, and wakeup publishing through the configured lane.

The session file is keyed by the resolved config path. If two runlane dev processes use the same config path, the most recently written session wins. Proxied commands also run against the task code already loaded in the dev process, so restart runlane dev after changing task definitions or runtime wiring.

runlane init config

init config writes the thin config adapter that the packaged binary loads. It does not load or start a runtime.

runlane init config --runtime ./runtime/runlane.ts

The generated file imports an application-owned runtime value or factory and exports the CLI config contract:

import { type RunlaneCliConfig } from '@runlane/cli'

import { createAppRunlane } from './runtime/runlane.ts'

export default {
  runtime: createAppRunlane,
} satisfies RunlaneCliConfig

Switches:

  • --runtime <specifier> is required. It is the module specifier imported from the generated config file.
  • --path <path> writes to a different config path. The default is runlane.config.ts.
  • --runtime-export <name> selects the export to import from the runtime module. The default is createAppRunlane; pass default for a default export.
  • --overwrite replaces an existing config file.

Existing config files are not overwritten by default. If the target file exists, choose a different --path or pass --overwrite deliberately.

runlane trigger

trigger creates one durable run for a task registered in the configured runtime's createRunlane({ tasks }) collection.

runlane trigger emails.send '{"userId":"user_123"}'
runlane trigger reports.daily '{"date":"2026-05-22"}' --idempotency-key daily_report_2026_05_22

The payload argument is JSON. Omit it only when the task schema accepts undefined.

Human output distinguishes a newly created run from an idempotency hit. Run detail output includes the run status, task, queue, environment, timestamps, counters, keys, source, payload, and metadata when those fields exist:

✓ created run_123
  status          queued
  task            emails.send
  queue           emails
  environment     default
  event sequence  1
  created         2026-05-23T20:15:26.000Z
  updated         2026-05-23T20:15:26.000Z
  attempts        0
  failures        0
  releases        0
  retries         0
  payload         {"userId":"user_123"}

⊝ returned existing run_123 (idempotency)
  status          queued
  task            emails.send
  queue           emails
  environment     default
  event sequence  1
  created         2026-05-23T20:15:26.000Z
  updated         2026-05-23T20:15:26.000Z
  attempts        0
  failures        0
  releases        0
  retries         0

hint  use runlane rerun run_123 to execute this work again

--json emits the structured TriggerRunResult with run and outcome.

When a matching runlane dev session is active for the same config path and the dev runtime uses process-local storage, this command proxies to that process before loading a new runtime. That is what makes the two-terminal local lane flow work:

# terminal 1
runlane dev

# terminal 2
runlane trigger emails.welcome '{"userId":"usr_demo_alice"}'

Switches:

  • --queue <queue> overrides the task queue.
  • --run-id <runId> supplies the run id.
  • --idempotency-key <key> deduplicates producer retries for the same logical work.
  • --idempotency-key-ttl <duration> sets terminal idempotency owner retention. It accepts a Runlane duration such as 30d or active.
  • --singleton-key <key> prevents overlapping active runs for the same logical resource.
  • --concurrency-key <key> partitions bounded queue capacity.
  • --meta <json> stores a JSON object on the run creation event.
  • --actor-id <id> records the operator actor id.

runlane dev

dev starts a storage-polling worker and a maintenance loop in one process. It is intended for local development against a configured runtime.

runlane dev --queue emails --concurrency 2 --tick-interval 5s

--once runs one maintenance pass, drains currently due work, and exits. That is useful for scripts and smoke tests:

runlane dev --once --max-runs 25

Long-running dev uses runlane.worker({ mode: WorkerMode.Poll }) and periodically calls runlane.tick(). --once runs tick(), starts a drain worker with WorkerMode.Drain, waits for that worker to close, and prints the tick summary plus worker stop line.

Human output for long-running dev is append-only lifecycle output. Proxied commands, executed runs, and maintenance ticks are printed as timestamped one-line records so terminal logs, CI captures, and redirected output stay readable:

2026-05-23T20:15:26.000Z command trigger ✓ created run_123
2026-05-23T20:15:26.050Z run succeeded run_123 task=emails.welcome queue=emails attempts=1
2026-05-23T20:15:30.123Z tick materialized=0 deliveryRequested=2 cancellationsFinalized=0 outboxPublished=2 outboxFailed=0 outboxDeadLettered=0

For shared lanes such as Postgres, runlane dev is still a storage-polling worker plus maintenance loop. With a Postgres+SQS lane, this is a local development convenience, not a production topology: it can execute runs from Postgres, but it does not receive or delete SQS messages. Production Postgres+SQS deployments need an SQS consumer such as the Lambda helper or long-running SQS consumer. Production polling deployments should use a polling lane/config that does not publish SQS wakeups for queues with no SQS consumer.

--once does not run a second maintenance tick after the drain worker exits. Run runlane tick explicitly afterward when a script intentionally wants a follow-up maintenance pass for newly due recovery work.

Switches:

  • --once runs one maintenance tick, drains due work, and exits.
  • --queue <queues...> limits worker acquisition to one or more queues.
  • --concurrency <count> sets the number of worker slots. The default is 1.
  • --max-runs <count> limits executed runs in --once mode.
  • --empty-poll-delay <duration> sets the polling worker sleep when no work is due. The default is 100ms. It is only valid without --once.
  • --tick-interval <duration> sets the maintenance loop interval. The default is 5s.
  • --schedule-materialization-limit <count> bounds due schedule occurrence materialization per tick.
  • --delivery-request-limit <count> bounds due delivery requests appended per tick.
  • --cancellation-finalization-limit <count> bounds expired cancellations finalized per tick.
  • --outbox-claim-limit <count> bounds outbox rows claimed for publishing per tick.

Advanced:

  • --worker-id <id> overrides the generated worker identity recorded on leases and outbox claims. It is for diagnostics, not routing; use --queue to choose which work this process handles.

runlane work

work starts a core storage-acquisition worker. Poll mode is the default and keeps running until the process receives a stop signal.

runlane work --queue emails --concurrency 4 --empty-poll-delay 250ms

Drain mode processes currently due work and exits when storage is idle or --max-runs is reached. Human output prints worker started, timestamped run ... lines for executed runs, and worker stopped.

runlane work --drain --queue emails --max-runs 100

This command scans storage. It is not an SQS consumer and does not delete SQS provider messages. Use the SQS transport Lambda helper or long-running SQS consumer for transport-delivered wakeups.

Switches:

  • --drain processes currently due work and exits when storage is idle or --max-runs is reached.
  • --queue <queues...> limits acquisition to one or more queues.
  • --concurrency <count> sets the number of worker slots. The default is 1.
  • --max-runs <count> limits executed runs in drain mode.
  • --empty-poll-delay <duration> sets poll sleep when no work is due. The default is 100ms. It is only valid without --drain.
  • --lease-duration <duration> overrides the run lease duration.
  • --heartbeat-interval <duration> overrides the run lease heartbeat interval.

Advanced:

  • --worker-id <id> overrides the generated worker identity recorded on leases. It is for diagnostics, not routing; use --queue to choose which work this process handles.

runlane tick

tick runs one maintenance pass over registered schedules, due released or retrying runs, cancellation finalization, and durable outbox publishing. Human output prints a tick complete card with counts for materialized schedules, delivery requests, finalized cancellations, and outbox publish results.

runlane tick --schedule-materialization-limit 100 --outbox-claim-limit 100

Bounds are optional. Omitted values use core defaults.

Switches:

  • --schedule-materialization-limit <count> bounds due schedule occurrence materialization.
  • --delivery-request-limit <count> bounds due delivery requests appended.
  • --cancellation-finalization-limit <count> bounds expired cancellations finalized.
  • --outbox-claim-limit <count> bounds outbox rows claimed for publishing.

runlane runs list

runs list reads operator run summaries in the configured runtime environment.

runlane runs list --status failed --queue emails --limit 50
runlane runs list --task emails.send --created-from 2026-05-01T00:00:00.000Z

Human output is an aligned table for scanning ids, statuses, tasks, queues, and due times. Use --json when another program needs the raw page and cursor.

Switches:

  • --cursor <cursor> continues a previous operator read page.
  • --limit <count> bounds returned run summaries.
  • --status <statuses...> filters by durable run status: cancellation_requested, cancelled, failed, queued, released, retrying, running, scheduled, or succeeded.
  • --task <taskIds...> filters by task id.
  • --queue <queues...> filters by queue.
  • --source-run <runId> filters by linked source run id.
  • --created-from <date> and --created-to <date> filter createdAt by inclusive ISO date/time bounds.
  • --updated-from <date> and --updated-to <date> filter updatedAt by inclusive ISO date/time bounds.
  • --run-at-from <date> and --run-at-to <date> filter runAt by inclusive ISO date/time bounds.
  • --sort-by <field> sorts by created_at, updated_at, or run_at.
  • --sort-direction <direction> sorts asc or desc.

runlane runs get

runs get reads one materialized run. Add --events to include append-only event history for that run. A missing run returns a structured run_not_found error.

runlane runs get run_123
runlane runs get run_123 --events --limit 100

Event history is sorted by sequence ascending so the output follows reducer order.

Human output renders the run as an indented detail card and appends an event timeline when --events is present. Use --json for the raw run and event page. Without --events, JSON output contains the run only because undefined fields are omitted.

Switches:

  • --events includes append-only run event history.
  • --cursor <cursor> continues a previous run event page.
  • --limit <count> bounds returned events.

runlane retry

retry creates a manual retry child from a failed source run. The source run remains terminal and unchanged.

runlane retry run_failed_123 --queue emails --actor-id ops@example.com

Use --run-id only when an operator process needs to supply a deterministic child run id.

Switches:

  • --queue <queue> overrides the child run queue.
  • --run-id <runId> supplies the child run id.
  • --actor-id <id> records the operator actor id.

runlane rerun

rerun creates a linked child from any terminal source run. It is for replaying completed work, not recovering a failure specifically.

runlane rerun run_succeeded_123 --queue emails

Like manual retry, rerun creates a new run and preserves the source run.

Switches:

  • --queue <queue> overrides the child run queue.
  • --run-id <runId> supplies the child run id.
  • --actor-id <id> records the operator actor id.

runlane cancel

cancel writes a durable cancellation request. Waiting runs become terminally cancelled. Running runs move to cancellation_requested and rely on cooperative task code observing context.signal.

runlane cancel run_123 --reason operator_requested --actor-id ops@example.com

Cancellation is not a process kill. If the active owner does not cooperate, maintenance finalizes cancellation after the cancellation-requested lease expires.

Switches:

  • --reason <reason> records an operator cancellation reason.
  • --actor-id <id> records the operator actor id.

runlane prune

prune deletes old terminal run history through storage adapters that report pruning support.

runlane prune --older-than 30d --status succeeded cancelled --limit 500

--older-than accepts a Runlane duration such as 30d or an ISO date/time. Status filters must be terminal statuses. When output includes a continuation cursor, pass it back with the same cutoff and statuses:

runlane prune --older-than 30d --status succeeded cancelled --cursor cursor_123

Switches:

  • --older-than <duration-or-date> is required. It accepts a Runlane duration such as 30d or an ISO date/time cutoff.
  • --status <statuses...> filters terminal statuses. Valid values are cancelled, failed, and succeeded.
  • --limit <count> bounds pruned runs.
  • --cursor <cursor> continues a previous prune page using the same cutoff and statuses.
  • --actor-id <id> records the operator actor id.

On this page