Runlane
Concepts

Current-Process Execution

Use runNow when the caller wants one durable attempt to start immediately.

Current-process execution creates a durable run, claims its lease, and executes one attempt in the caller's process. Use it when application code intentionally wants the result of the first attempt now, but still wants Runlane's payload validation, run history, leases, retry/release outcomes, singleton enforcement, and operator visibility.

This is not a sync lane. runNow() is a core runtime method over the same storage contract as trigger(), workers, and delivered wakeups. Task handlers can still await I/O, observe context.signal, return context.release(...), throw retryable errors, or be cancelled by operators.

const run = await runlane.runNow(syncQuickBooksInvoices, { userId: 'user_123' })

With explicit creation and lease controls:

import { ActorType, asId } from '@runlane/core'

const run = await runlane.runNow(
  syncQuickBooksInvoices,
  { userId: 'user_123' },
  {
    actor: { type: ActorType.Operator, id: 'user_123' },
    idempotencyKey: asId<'idempotency_key'>('quickbooks_sync_button_user_123'),
    singletonKey: asId<'singleton_key'>('quickbooks_invoices_user_123'),
    traceCarrier,
  },
)

When To Use It

Use runNow() for request-path or operator-path work where the caller wants immediate durable execution and can afford to wait for one attempt. Common examples are admin buttons, explicit user sync buttons, local development flows, and small workflows where the first attempt should complete before the HTTP response is chosen.

Use trigger() when the caller only needs to persist work and return. Use workers, executeNext(), executeDelivery(), or SQS consumers when another process or provider message owns execution.

runNow() executes exactly one attempt. If that attempt returns context.release(...), the run becomes released. If it fails retryably with retry budget left, the run becomes retrying. Later tick() writes a fresh delivery request and outbox row through the normal maintenance path. runNow() does not loop through retries or releases inline.

Do not use runNow() as a way to bypass background execution policy. It still resolves the task through the runtime's authoritative task registry, validates payload before durable creation, resolves queues and creation keys like trigger(), requires the selected lane capabilities for idempotency, singleton, and concurrency keys, and validates the persisted payload again before user code runs.

API

runlane.runNow(task, payload?, options?) accepts the same payload rules as trigger(): the payload is validated before durable creation and again before user code runs from the persisted payload.

Creation options:

OptionDefaultWhat it controls
actor{ type: ActorType.System }Actor recorded on the initial run.created event.
concurrencyKeyTask-level concurrencyKeyPer-run bounded-queue partition override.
idempotencyKeyTask-level idempotencyKeyPer-run idempotency owner override. Duplicate active or retained owners return the existing run without executing another inline attempt.
idempotencyKeyTTLTask-level idempotencyKeyTTL, then 30dRetention for the selected idempotency owner. Requires a task or explicit idempotency key.
metaNoneJSON object stored on the initial run event and run projection for operator context.
queueTask queue or runtime default queuePer-run queue definition override. Recovered, retried, or released inline runs keep this queue for future workers.
runIdGenerated run_${uuid}Caller-supplied run id for external correlation. It should be rare.
singletonKeyTask-level singletonKeyPer-run active-resource lock override.
traceCarrierNoneSmall string map for trace headers or request correlation.

Execution options:

OptionDefaultWhat it controls
leaseDurationcontractDefaults.lease.duration (5m)Durable ownership window written with the initial inline lease and each heartbeat.
heartbeatIntervalHalf of leaseDurationHow often core writes run.lease_heartbeat while the inline attempt is still running. Must be shorter than leaseDuration.
signalNoneLocal abort signal linked into TaskContext.signal. It does not create a durable cancellation request by itself, and an already-aborted signal still allows creation before the handler observes the abort.
workerIdRuntime worker idDiagnostic owner recorded on the inline lease. It does not route work.

runNow() returns a RunRecord. For a new inline run, the record is returned after the first attempt outcome is durably persisted. If storage idempotency returns an existing owner, no inline attempt runs and the existing active or retained terminal run is returned as-is.

For a new inline attempt, the returned status can be:

StatusMeaning
succeededThe handler completed without returning a release.
failedThe handler failed and no retry is scheduled. Unknown thrown errors are persisted as ErrorCode.TaskFailed with a stable public message.
retryingThe handler failed retryably and retry budget remains. A later tick() requests delivery.
releasedThe handler returned context.release(...). A later tick() requests delivery when the release is due.
cancelledA durable operator cancellation request was observed and the attempt completed cancellation.

Validation and configuration errors reject before durable creation. Storage capability errors for selected keys reject before durable creation. If storage, projection, heartbeat, or outcome persistence fails after creation, runNow() rejects with a structured RunlaneError; it does not convert framework failures into task failures.

Unknown handler errors are caught inside the task-attempt boundary and persisted as ErrorCode.TaskFailed with the stable public message Task failed. Structured RunlaneError failures preserve their code, metadata, and retryability, but user-facing failure messages are still normalized.

If an inline heartbeat loses lease ownership with StorageConflict, core aborts TaskContext.signal, abandons the task result, and runNow() rejects with StorageConflict because it cannot prove the current process still owns the run. Other heartbeat or persistence failures reject for the same reason: ownership or durable outcome is not trustworthy.

Event Model

The initial inline append writes the created event and the lease claim atomically:

run.created
run.lease_claimed
run.started
run.lease_heartbeat...
run.succeeded | run.failed | run.retry_scheduled | run.released | run.cancelled

There is no initial run.delivery_requested event and no initial outbox row. runNow() does not call transport for the first attempt, even when the runtime's trigger dispatch policy is eager.

That atomic lease matters. A storage-polling worker scanning immediately after creation sees an already leased running run, not a queued run it can claim before the caller starts the inline attempt.

If the process dies after run.created + run.lease_claimed but before an outcome is persisted, the run remains running until the lease expires. A later tick() appends run.delivery_requested, creates the outbox row, and normal workers or transport consumers recover the run.

Local caller abort and durable operator cancellation are different. Passing signal only affects the local task context. If the handler reacts by throwing, that is persisted as a task failure according to the task's retry policy. Durable cancellation comes from the operator API; when the inline attempt observes cancellation_requested, core aborts the task context and persists run.cancelled if the run is still owned.

Choosing An Execution Path

APICreates a runExecutes user codeHow it chooses workUse when
runNow(task, payload, options)YesYes, one current-process attemptThe task and payload supplied by the callerThe caller wants one durable attempt to start immediately.
trigger(task, payload, options)YesNoThe task and payload supplied by the callerThe caller wants durable background work and can return after persistence.
executeNext(options)NoYes, at most one attemptScans storage for one due run in the allowed queuesA process intentionally owns storage polling for one unit of work.
worker(options)NoYes, repeated attemptsLoops over executeNext() in poll or drain modeA process should continuously or boundedly drain storage-acquired work.
executeDelivery(message, options)NoYes, at most one delivered runReads the specific run named by a transport messageA transport already acquired a provider message, such as SQS Lambda.
SQS consumersNoThrough executeDelivery()Receives provider messages, parses Runlane delivery envelopes, and acknowledges handled messagesAWS SQS owns wakeup delivery and redrive behavior.

Do not implement current-process execution by calling trigger() followed by executeNext(). That path may create a delivery request and outbox row, and under concurrency executeNext() can execute a different due run. runNow() creates the run and its lease in one storage append, then executes that exact run.

Singleton Example

Use the same singleton key for the protected resource everywhere the work can be created. A UI button and a cron path that both sync QuickBooks invoices for one user should resolve the same singleton key.

import { asId, queue, task } from '@runlane/core'

const quickBooksQueue = queue({
  name: asId<'queue'>('quickbooks'),
  concurrencyLimit: 2,
})

const syncQuickBooksInvoices = task({
  id: asId<'task'>('quickbooks.invoices.sync'),
  queue: quickBooksQueue,
  schema: quickBooksSyncSchema,
  schedule: [
    {
      id: asId<'schedule'>('quickbooks.invoices.hourly.user_123'),
      cron: '0 * * * *',
      timeZone: 'UTC',
      payload: { userId: 'user_123' },
    },
  ],
  singletonKey: (payload) => asId<'singleton_key'>(`quickbooks_invoices_${payload.userId}`),
  async run(payload, context) {
    await syncInvoicesForUser(payload.userId, { signal: context.signal })
  },
})

await runlane.runNow(syncQuickBooksInvoices, { userId: 'user_123' })

If a schedule has already created an active invoice sync for user_123, the inline UI path fails with the same storage-enforced singleton conflict instead of overlapping the sync. The local lane enforces singleton keys in memory so app tests can exercise this definition, but that does not prove production locking. Production singleton correctness requires a lane whose storage reports and implements enforcesSingleton.

On this page