A job run: one logical piece of long background work (V207).
Oban executes it batch by batch; this row is what an admin watches and
controls. See PhoenixKit.Jobs for the API and
dev_docs/plans/2026-10-03-job-runs.md for the design. Go through
PhoenixKit.Jobs rather than writing rows: every transition is one
transaction that also dispatches the next batch and writes the history.
States
queued— started (or resumed, or restarted), waiting for a batch;running— a batch is executing, or the next one is waiting for its turn;pausing/cancelling— a pause or cancel was asked while a batch holds the run; the batch finishes, then the run becomespaused/cancelled;paused,completed,failed,cancelled.
The first five are active: at most one run per kind and scope may be in one of them (a partial unique index), so a retry cannot start while the old run is still draining.
The execution protocol
generation counts dispatches: every Oban job carries the generation it was
made for, and one of an older generation is inert. claim_token /
claimed_at say a batch holds the run now. restart_seq counts triggers that
asked for a fresh pass and restart_ack the last one a batch has taken in.
rescues / last_rescued_at are the sweeper's durable budget, and
oban_job_id the current dispatch.
Summary
Functions
Whether the run has not finished.
The states of an unfinished run: at most one per kind and scope.
Whether a batch holds the run right now.
Whether a script (run_inline/3) holds the run, not an Oban batch.
Seconds a script's claim stands without a heartbeat.
Whether a script owns the run between its batches: it holds no batch claim but has not given the run up either (it is waiting out a delay).
The changeset a new run is inserted with.
Whether the lease of a script's claim has run out at now. The lease runs from
the latest sign of life — the claim, a heartbeat, or the time the script asked
its next batch to wait for (wake_at) — so a script waiting out a long delay
between batches holds its run for the delay and a lease after it.
The ways a run can have been started.
A whole number between 0 and 100, or nil when the total is not known.
The run's scope as {type, uuid}, or :site when it is about nothing in
particular.
Seconds until wake_at, never negative; 0 when the run has no delay pending.
Every state a run can be in.
Whether the run has finished, one way or another.
The states a run never leaves.
Types
@type t() :: %PhoenixKit.Jobs.Run{ __meta__: term(), args: term(), cancelled_at: term(), cancelled_by_uuid: term(), claim_owner: term(), claim_token: term(), claimed_at: term(), cursor: term(), done: term(), error: term(), failed_count: term(), finished_at: term(), generation: term(), heartbeat_at: term(), inserted_at: term(), interruptions: term(), kind: term(), last_rescued_at: term(), mode: term(), module: term(), oban_job_id: term(), owner_token: term(), paused_at: term(), paused_by_uuid: term(), rescues: term(), restart_ack: term(), restart_seq: term(), result: term(), scope_type: term(), scope_uuid: term(), started_at: term(), started_by_uuid: term(), state: term(), title: term(), total: term(), updated_at: term(), uuid: term(), wake_at: term() }
Functions
Whether the run has not finished.
The states of an unfinished run: at most one per kind and scope.
Whether a batch holds the run right now.
Whether a script (run_inline/3) holds the run, not an Oban batch.
Seconds a script's claim stands without a heartbeat.
Whether a script owns the run between its batches: it holds no batch claim but has not given the run up either (it is waiting out a delay).
The changeset a new run is inserted with.
@spec lease_expired?(t(), DateTime.t()) :: boolean()
Whether the lease of a script's claim has run out at now. The lease runs from
the latest sign of life — the claim, a heartbeat, or the time the script asked
its next batch to wait for (wake_at) — so a script waiting out a long delay
between batches holds its run for the delay and a lease after it.
The ways a run can have been started.
@spec percent(t()) :: non_neg_integer() | nil
A whole number between 0 and 100, or nil when the total is not known.
The run's scope as {type, uuid}, or :site when it is about nothing in
particular.
@spec seconds_until_wake(t(), DateTime.t()) :: non_neg_integer()
Seconds until wake_at, never negative; 0 when the run has no delay pending.
Every state a run can be in.
Whether the run has finished, one way or another.
The states a run never leaves.