The transitions of a job run, as pure functions (no database, no clock, no
Oban): transition/3 takes a run, an event and the time, and answers the fields to
change and the effects to carry out, or why the event does not apply.
PhoenixKit.Jobs.Engine does the I/O — it locks the run row, calls this,
writes the changes, inserts the Oban job a {:dispatch, seconds} effect asks
for and the Activity entry a {:log, …} effect asks for, all in one
transaction. Keeping the rules here makes them testable without a database
(dev_docs/plans/2026-10-03-job-runs.md, §3.2, §14 R1, R2, R5, R7).
Events
{:claim, token}— a batch is about to work. Needsqueuedorrunningand no claim held. A pending restart is taken in here.{:checkpoint, token, outcome}— the batch holdingtokenis done.outcomeis{:more, progress, opts},{:done, progress, result},{:snooze, seconds},{:fail, message}(final) or{:release, message}(an error Oban will retry).{:pause, actor},{:resume, actor},{:cancel, actor}:request_restart— a trigger asked for a fresh pass{:heartbeat, token}{:rescue, limit}— the sweeper found the dispatch lost; refused while a batch holds the run{:fail, message}— the run cannot go on (a job the sweeper found discarded); refused while a batch holds the run — a batch ends its own run through{:checkpoint, token, {:fail, message}}
Precedence when a batch ends
| State | Batch returns more / snoozes | Batch is done |
|---|---|---|
running, no restart pending | next batch dispatched | completed |
running, restart pending | pass restarts (queued, counters reset) | pass restarts |
pausing | paused, nothing dispatched | completed (nothing is left to pause) |
cancelling | cancelled | cancelled |
A pending restart never overrides a pause or a cancel; it waits.
Summary
Functions
Applies event to run at now: {:ok, changes, effects} — changes is a
map of fields to write, effects what to do besides — or {:error, reason}.
Types
@type actor() :: String.t() | nil
@type effect() :: {:dispatch, non_neg_integer()} | {:log, String.t(), map()}
@type error() ::
:inactive
| :claimed
| :claim_lost
| :already_pausing
| :already_paused
| :already_cancelling
| :draining
| :not_paused
| :cancelling
| :finished
| :not_active
@type progress() :: %{ optional(:cursor) => map(), optional(:done) => non_neg_integer(), optional(:failed) => non_neg_integer(), optional(:total) => non_neg_integer() | nil }
Functions
@spec transition(PhoenixKit.Jobs.Run.t(), event(), DateTime.t()) :: {:ok, map(), [effect()]} | {:error, error()}
Applies event to run at now: {:ok, changes, effects} — changes is a
map of fields to write, effects what to do besides — or {:error, reason}.