Runlane
Concepts

Cancellation

Cancellation requests stop work cooperatively.

Cancellation is the semantic model for operator or system requests that ask a run to stop. Any runtime or operator surface that supports cancellation must preserve audit history and use cooperative stopping instead of pretending it can hard-kill arbitrary user code safely.

Runlane cancellation is durable and cooperative. The cancellation request is written to run history before task code is notified, and user code decides where it is safe to stop. Runlane does not kill JavaScript execution, cancel network calls that were not passed an abort signal, roll back side effects, or erase the run.

Task handlers receive both context.signal and context.isCancellationRequested(). Pass the signal to cancellable I/O and check it inside long loops or cleanup boundaries.

const importContacts = task({
  id: 'contacts.import',
  schema: importContactsSchema,
  async run(payload, context) {
    for await (const contact of crm.streamContacts(payload.accountId, { signal: context.signal })) {
      if (context.isCancellationRequested()) {
        return
      }

      await upsertContact(contact, { signal: context.signal })
    }
  },
})

const { run } = await runlane.trigger(importContacts, { accountId: 'acct_123' })

await runlane.runs.cancel(run.id, {
  actor: { type: ActorType.Operator, id: 'ops@example.com' },
  reason: 'operator_requested',
})

Waiting runs

A run that is waiting in queued, scheduled, released, or retrying state has no active user code to interrupt. Cancelling it appends run.cancelled immediately and moves the run to terminal cancelled.

The run remains in append-only history with the actor, reason, trace carrier, and metadata supplied to runs.cancel(). Terminal runs cannot be cancelled again or moved back to active state.

Running runs

Running runs have an active lease and may already be inside user code, so cancellation happens in two phases:

  1. runs.cancel() appends run.cancellation_requested and the run becomes active cancellation_requested.
  2. The worker stops the attempt cooperatively. When the attempt returns successfully after observing cancellation, core appends run.cancelled.

cancellation_requested is not runnable. Workers do not acquire it as fresh work, and delivery recovery does not make it executable again. It remains active only so the current owner can stop cleanly or so maintenance can finalize it after the lease expires.

Same-process cancellation is fast. If the operator API and the running attempt share a runtime instance, an in-memory registry matches the run id, worker id, and lease token, then aborts the task's context.signal immediately after the durable cancellation request is appended.

Cross-process cancellation is still durable. A different worker process observes the cancellation_requested state through lease monitoring. Once observed, it aborts its local context.signal and stops extending the lease so an uncooperative attempt cannot keep the run alive forever.

Repeated runs.cancel() calls on a cancellation_requested run do not append duplicate cancellation-requested events. They return the current run and re-notify same-process task code when possible.

Completion after cancellation

Cancellation changes how successful completion is interpreted. If user code returns while the run is cancellation_requested, core records terminal cancelled instead of succeeded or released.

Cancellation does not hide failures. If task code throws after seeing context.signal.aborted, or if a retryable failure races with cancellation, core records terminal failed. A cleanup failure is still a task failure, not a successful cancellation, and retry scheduling is not used once cancellation has been requested.

If maintenance finalizes the run before a cooperative completion append lands, execution returns the stored terminal cancellation when it can prove the stored run is already cancelled. Persistence conflicts that cannot be reconciled still surface as framework errors instead of being converted into task failures.

Maintenance finalization

runlane.tick() finalizes stale cancellation requests. It scans cancellation_requested runs whose current lease has expired, re-reads each run, and appends run.cancelled with the system actor. The default tick bound is controlled by the maintenance cancellation-finalization limit.

This is the recovery path for workers that crash, lose their process, ignore the signal, or never reach a cooperative stop point. Maintenance waits for lease expiry so a live owner has a chance to clean up before the system records terminal cancellation.

Limits

Cancellation is not preemption. Code that ignores context.signal, CPU-bound loops that never yield, and third-party calls that do not accept an AbortSignal may continue running until they return, throw, or the process exits. Runlane can stop renewing the lease and finalize durable state, but it cannot undo side effects already performed by user code.

Worker shutdown uses the same task-context signal but is not the same as an operator cancellation request. Calling worker.stop() or aborting the worker signal aborts context.signal; if the task returns normally and no durable cancellation was requested, the run can still succeed.

Invariants

Cancellation is an operator control surface, not a data-erasing shortcut. Cancelling a run must preserve audit history, respect active leases, and avoid mutating terminal runs back into active states.

On this page