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-localConfig 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 RunlaneCliConfigTypeScript 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.exitCodeUse --json before the command to emit machine-readable output:
runlane --json runs list --status failed --limit 20| Switch | Behavior |
|---|---|
-c, --config <path> | Loads a config module from a non-default path for runtime-backed commands. Relative paths resolve from the current working directory. |
--json | Emits 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.tsThe 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 RunlaneCliConfigSwitches:
--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 isrunlane.config.ts.--runtime-export <name>selects the export to import from the runtime module. The default iscreateAppRunlane; passdefaultfor a default export.--overwritereplaces 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_22The 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 as30doractive.--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 25Long-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=0For 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:
--onceruns 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 is1.--max-runs <count>limits executed runs in--oncemode.--empty-poll-delay <duration>sets the polling worker sleep when no work is due. The default is100ms. It is only valid without--once.--tick-interval <duration>sets the maintenance loop interval. The default is5s.--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--queueto 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 250msDrain 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 100This 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:
--drainprocesses currently due work and exits when storage is idle or--max-runsis reached.--queue <queues...>limits acquisition to one or more queues.--concurrency <count>sets the number of worker slots. The default is1.--max-runs <count>limits executed runs in drain mode.--empty-poll-delay <duration>sets poll sleep when no work is due. The default is100ms. 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--queueto 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 100Bounds 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.000ZHuman 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, orsucceeded.--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>filtercreatedAtby inclusive ISO date/time bounds.--updated-from <date>and--updated-to <date>filterupdatedAtby inclusive ISO date/time bounds.--run-at-from <date>and--run-at-to <date>filterrunAtby inclusive ISO date/time bounds.--sort-by <field>sorts bycreated_at,updated_at, orrun_at.--sort-direction <direction>sortsascordesc.
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 100Event 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:
--eventsincludes 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.comUse --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 emailsLike 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.comCancellation 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_123Switches:
--older-than <duration-or-date>is required. It accepts a Runlane duration such as30dor an ISO date/time cutoff.--status <statuses...>filters terminal statuses. Valid values arecancelled,failed, andsucceeded.--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.