Runlane
Reference

Core Reducer

Project run events into materialized run state.

@runlane/core owns run lifecycle semantics. The low-level public primitive is projectRunEvents(), a pure reducer that turns append-only RunEvent values into the RunRecord projection storage adapters persist.

Most application code should never call this directly. Runtime APIs such as trigger(), runNow(), worker(), tick(), and runs.cancel() call it before they ask storage to append lifecycle events. Adapter authors need the contract because storage must persist the supplied events and supplied projection atomically without reimplementing status, counter, lease, retry, release, or cancellation rules.

Short example

The reducer can create a run from its first durable event:

import { asId, contractDefaults, RunEventType } from '@runlane/contracts'
import { projectRunEvents } from '@runlane/core'

const environment = { name: 'production' }
const runId = asId<'run'>('run_123')
const now = new Date()

const projectedRun = projectRunEvents({
  expectedSequence: contractDefaults.run.newEventSequence,
  events: [
    {
      actor: contractDefaults.actor.system,
      environment,
      occurredAt: now,
      payload: { userId: 'user_123' },
      queue: contractDefaults.queue,
      runId,
      taskId: asId<'task'>('emails.send'),
      type: RunEventType.Created,
    },
  ],
})

The returned RunRecord has eventSequence: 1, zeroed counters, status: "queued", and the durable identity fields copied from the created event.

Appending later events starts from the current materialized run:

const updatedRun = projectRunEvents({
  currentRun: projectedRun,
  expectedSequence: projectedRun.eventSequence,
  events: [
    {
      actor: contractDefaults.actor.system,
      delivery: {
        availableAt: now,
        environment,
        queue: contractDefaults.queue,
        requestedAt: now,
        runId,
      },
      environment,
      occurredAt: now,
      runId,
      type: RunEventType.DeliveryRequested,
    },
  ],
})

Sequence contract

expectedSequence must match the current run sequence. Use contractDefaults.run.newEventSequence when creating a run and the current RunRecord.eventSequence when appending to an existing run. A stale sequence throws RunlaneError with ErrorCode.StorageConflict so callers can retry the read-project-append operation.

The reducer assigns projected sequences in memory as expectedSequence + event index + 1. Storage still owns durable event ids and must reject appends if its current sequence no longer matches the command.

Projection failures use two different error classes:

ConditionError
expectedSequence is stale against currentRun.eventSequenceRunlaneError, ErrorCode.StorageConflict, retryable: true
expectedSequence is negative, non-integer, or unsafeRunlaneError, ErrorCode.InvariantViolation
events is emptyRunlaneError, ErrorCode.InvariantViolation
Event order, status, lease, attempt, queue, run id, or environment is impossibleRunlaneError, ErrorCode.InvariantViolation

Adapters should preserve this distinction. A stale sequence is a normal lost race. An impossible projection means core, storage, or persisted history is inconsistent.

Lifecycle rules

run.created is the only valid first event. It creates a queued run with zero attempts, failures, retries, and releases. The run projection copies durable identity and creation fields from this event: runId, environment, taskId, queue, payload, runAt, concurrencyKey, idempotencyKey, singletonKey, source, traceCarrier, meta, createdAt, and updatedAt.

Every later event must target the same runId and environment as the current projection. Terminal runs reject all later lifecycle events; rerun and manual retry create new source-linked runs instead of mutating terminal history.

EventProjection effect
run.delivery_requestedRecords durable wakeup intent. It can wake queued runs, due scheduled/retrying/released runs, or running runs whose lease has expired. It sets runAt to delivery.availableAt, clears the lease, and sets status to scheduled when availableAt is later than occurredAt; otherwise it sets status to queued. The delivery intent must target the same environment, run id, and queue as the run.
run.lease_claimedMoves runnable work to running, records the supplied lease, and clears dispatch reservation fields. Queued runs are runnable at runAt ?? updatedAt; scheduled, retrying, and released runs are runnable only once runAt is due; running runs are runnable again only after their lease expires.
run.lease_heartbeatReplaces the current lease while keeping the status and counters unchanged. The run must be running or cancellation_requested, and the heartbeat must use the current lease token and worker id.
run.startedStarts the next attempt. The run must be running with an active lease, and event.attempt must equal counters.attempts + 1. The reducer increments attempts, clears any previous failure, and records startedAt.
run.succeededCompletes the current attempt and terminally sets status to succeeded. It clears lease and dispatch reservation fields, clears failure, and records finishedAt.
run.failedCompletes the current attempt and terminally sets status to failed. It increments failures, stores failure, clears lease and dispatch reservation fields, and records finishedAt.
run.retry_scheduledCompletes the current attempt as failure pressure without making the run terminal. It increments failures and retries, stores failure, clears lease and dispatch reservation fields, sets runAt to retryAt, and sets status to retrying.
run.releasedCompletes the current attempt as business waiting. It increments releases, clears lease and dispatch reservation fields, sets runAt to resumeAt, and sets status to released without incrementing failures or retries.
run.cancellation_requestedMarks a running leased run as cancellation_requested while keeping the lease. This lets cooperative cancellation reach the handler through its abort signal and then persist a terminal or waiting outcome.
run.cancelledTerminally cancels a queued, scheduled, retrying, released, or cancellation-requested run. Direct cancellation of a still-running run is rejected until cancellation has been requested. The reducer clears lease, dispatch reservation fields, and failure, then records finishedAt.

Attempt completion events are run.succeeded, run.failed, run.retry_scheduled, and run.released. They require a running or cancellation_requested run, require at least one started attempt, and must reference the current attempt number. The reducer does not infer attempt numbers from event order.

Projection Fields

RunRecord is the materialized view of durable history. These fields have reducer-owned semantics:

FieldReducer rule
eventSequenceFinal sequence after applying all events in the command.
statusDerived only from the event transition rules above.
counters.attemptsSet by run.started; must advance by exactly one.
counters.failuresIncremented by run.failed and run.retry_scheduled.
counters.retriesIncremented only by run.retry_scheduled.
counters.releasesIncremented only by run.released.
runAtCopied from run.created, then updated by delivery, retry, and release events.
startedAtUpdated by the latest run.started.
finishedAtSet only by terminal success, failure, or cancellation. Retry and release leave it unset.
failureSet by failed and retry-scheduled outcomes; cleared by started, succeeded, and cancelled transitions.
leaseSet by lease claim and heartbeat. Cleared when delivery recovery, terminal outcomes, retry, release, or cancellation move the run out of active execution.
dispatchedAt / dispatchExpiresAtSet only when a delivery request carries delivery.dispatchExpiresAt, which represents a bounded-queue dispatch reservation. Unbounded delivery requests clear both fields.
sourceCopied from run.created. Triggered, scheduled, rerun, and manual-retry lineage is not rewritten by later events.
traceCarrierCopied from run.created, then refreshed by delivery as delivery.traceCarrier ?? event.traceCarrier ?? currentRun.traceCarrier.
metaCopied from run.created. Later event meta remains in event history; it does not rewrite RunRecord.meta.

The reducer does not create outbox rows. It only projects fields that storage and runtime code use when deciding whether outbox, dispatch, lease, and operator behavior is valid.

Adapter boundary

Adapters should treat the projected run as caller-supplied truth that must be checked, not silently trusted. A storage adapter should verify the command environment, run id, expected sequence, projected final sequence, and command-specific ownership before committing.

appendRunEvents() must compare the current persisted sequence with expectedSequence before validating projection invariants. If it does not match, reject with ErrorCode.StorageConflict and StorageConflictKind.EventSequence; do not report a stale append as a projection bug.

When an append succeeds, storage commits these records atomically:

  • append-only event-history rows with storage-assigned ids and monotonic per-run sequences
  • the supplied projectedRun
  • storage-owned idempotency or singleton ownership updates
  • outbox rows derived from any run.delivery_requested events

reserveRunDispatch(), claimRunLease(), and heartbeatRunLease() also receive core-projected events and projected runs. Storage owns the atomic capacity, dispatch-reservation, lease-ownership, and sequence checks for those operations, but it should not recompute the materialized lifecycle projection.

The returned RunEventRecord values must be the exact rows persisted to history, including ids and sequence numbers. Do not generate one event id for storage and another for the API result.

See Adapter Authoring for the full storage contract and Run Lifecycle Vocabulary for stable status and event names.

On this page