Runlane
Concepts

Pruning

Prune old terminal data without touching active runs.

Pruning is the retention semantic for removing old terminal run data without touching active work.

Only terminal runs are eligible: succeeded, failed, and cancelled. Active statuses such as queued, running, retrying, released, scheduled, and cancellation_requested are rejected before storage is called. A stuck active run is an operational problem to inspect or cancel, not data to delete silently.

Retention scope

Prune filters are environment-scoped. This lets operators apply retention rules to one environment without touching another. Filters may also restrict eligible terminal statuses, such as pruning old succeeded and cancelled runs while keeping failed runs longer. When statuses is omitted, core sends the full terminal status set to storage.

olderThan is required. It may be a Runlane duration string such as 30d, resolved relative to the runtime clock when prune() is called, or an explicit Date cutoff. Core resolves either form into a concrete Date before calling storage. First-party local and Postgres storage prune terminal runs whose materialized run updatedAt is before that cutoff.

runlane.runs.prune() is available only when the selected lane's storage reports storage.prunesRuns: true. Otherwise core rejects the call with CapabilityUnsupported and does not attempt a best-effort cleanup.

Operator consequences

Pruning is irreversible from the public API perspective. After old run history is pruned, operators should not expect to inspect the removed event history or payload through normal run reads.

Use pruning only through an explicit maintenance surface with clear age and status boundaries. Pass a bounded limit for large histories and continue with nextCursor until no cursor is returned.

await runlane.runs.prune({
  actor: { type: ActorType.Operator, id: 'ops@example.com' },
  olderThan: '30d',
  statuses: [RunStatus.Succeeded, RunStatus.Cancelled],
  limit: 500,
})

The prune command sent to storage includes prunedAt from the runtime clock and prunedBy from actor. If actor is omitted, core uses contractDefaults.actor.operator.

When limit is omitted, storage prunes at most contractDefaults.pruning.batchLimit runs. Cursor continuations are tied to the same retention filter.

If pruning returns nextCursor, pass it back with the same environment, olderThan, and status set. Public prune cursors wrap the adapter cursor and the retention scope. Core rejects continuations when the cursor environment, olderThan, or statuses do not match. When olderThan is a duration string, core freezes the computed cutoff into the cursor so later continuation calls do not drift forward in time; pass the same duration string rather than recalculating a new date yourself.

const firstPage = await runlane.runs.prune({
  olderThan: '30d',
  statuses: [RunStatus.Succeeded, RunStatus.Cancelled],
  limit: 500,
})

if (firstPage.nextCursor !== undefined) {
  await runlane.runs.prune({
    cursor: firstPage.nextCursor,
    olderThan: '30d',
    statuses: [RunStatus.Succeeded, RunStatus.Cancelled],
    limit: 500,
  })
}

CLI

The CLI command maps directly to runlane.runs.prune() after loading the configured runtime:

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

--older-than is required and accepts either a Runlane duration or an ISO 8601 date/time. Ambiguous date strings such as 05/01/2026 are rejected before the runtime is loaded. --status accepts terminal status names only: succeeded, failed, and cancelled.

When CLI output includes nextCursor, continue with the same cutoff and statuses:

runlane prune --older-than 30d --status succeeded cancelled --cursor <nextCursor>

Use --actor-id <id> to pass an operator actor id. The CLI turns it into the prune actor; without it, core falls back to the default operator actor.

On this page