PhoenixKit.Jobs (phoenix_kit v2.55.1)

Copy Markdown View Source

Job runs: long background work an admin can watch and control.

A run is a durable record of one logical piece of work — a backfill, a reconcile of one library, an import, a broadcast — that Oban executes batch by batch. Pause, resume, cancel and retry are state changes on the run; the worker reads the state before every batch and never queues the next one for a run that was stopped. Jobs are core and always on: Admin → Jobs lists the runs, their progress and history, and jobs.manage controls them.

The design, and why it is shaped this way, is dev_docs/plans/2026-10-03-job-runs.md.

Declaring a kind

A module implements PhoenixKit.Jobs.Kind for each kind of run it has and lists them in PhoenixKit.Module.job_kinds/0.

Starting a run

A person, with a scope (the permission is checked against the active role):

PhoenixKit.Jobs.start(scope, MyApp.Jobs.ImportRows, :site, args: %{"file" => "a.csv"})

Boot, cron, a Mix task or a trigger — the trusted door, which never takes the place of a user:

PhoenixKit.Jobs.System.start(MyApp.Jobs.ImportRows, :site, mode: :cron)

A second start while a run of the kind and scope is active returns that run ({:ok, run, :existing}); a kind with restart/0 returning :restart also asks it to begin a fresh pass.

Controls

pause/2, resume/2, cancel/2 and retry/2 take a scope and a run (or its uuid). A pause or cancel while a batch is executing is a request: the run is pausing / cancelling until the batch finishes, and resume is refused meanwhile. Each control returns {:ok, run} or {:error, reason} — :unauthorized, :unknown_kind, :control_not_offered, :not_found or the state machine's (PhoenixKit.Jobs.StateMachine).

Reading

list_runs/1, count_runs/1, get_run/1, active_run/2, history/1. Live changes arrive over PhoenixKit.Jobs.Events.

Summary

Functions

The active run of kind for run_scope, or nil.

Whether scope may start a run of kind (jobs.manage and the kind's own permission against the active role) — what a screen asks before it offers a button that starts one.

Cancels a run. A batch already executing finishes first; its work is kept.

The controls scope may offer on run right now: the state allows them, the kind offers them, and the person holds jobs.manage and the kind's own permission. For the Jobs page, which shows only what would work.

How many runs match the filters of list_runs/1.

disable_system() deprecated

Does nothing: Jobs is always on. Kept for callers of the module this used to be.

enable_system() deprecated

Does nothing: Jobs is always on. Kept for callers of the module this used to be.

enabled?() deprecated

Always true: Jobs is core. Kept for callers of the module this used to be; there is no toggle.

The Oban job counts by state and whether Jobs is on.

Job statistics from the Oban jobs table (read through Oban's configured prefix; all zeros where no Oban instance runs in this VM, and a database error is raised, not hidden).

A run, or nil.

Tells the engine a long batch is still alive, and checks that it still holds the run. Call between steps of a batch that may run for minutes. :ok, or {:error, :claim_lost} when something else holds the run now (stop working).

The Activity entries of one run, oldest first.

The kind modules known now (PhoenixKit.Jobs.Kinds.all/0).

Runs, newest first. Filters (all optional): :module, :kind, :state (one or a list; :active for the unfinished ones), :scope (:site or {type, uuid}), :limit (default 25), :offset.

Pauses a run (see the moduledoc for what a pause while a batch executes means).

Resumes a paused run. Refused while the run is still pausing.

Starts a new run like a failed or cancelled one (same kind, scope and args), recording which it retries. A finished run is never changed.

Runs a run in the calling process to its end (or until it is paused or cancelled by someone else): what a Mix task or a script does instead of queueing. It goes through the same claim, checkpoint and failure rules as a queued run, and refuses a run a batch holds ({:error, :claimed}). Mode defaults to "script"; no Oban job is made. :on_progress is called with the run after every batch that made progress (a Mix task prints from it). A {:snooze, s} or a batch's schedule_in: is waited out — in short slices, so a pause or a cancel ends the wait.

Starts a run of kind for run_scope on behalf of the person behind scope. Options: :args. Returns {:ok, run, :started | :existing} or an error.

Types

kind()

@type kind() :: module() | String.t()

run_scope()

@type run_scope() :: :site | {atom() | String.t(), String.t()}

Functions

active_run(kind, run_scope \\ :site)

@spec active_run(kind(), run_scope()) :: PhoenixKit.Jobs.Run.t() | nil

The active run of kind for run_scope, or nil.

can_start?(scope, kind)

@spec can_start?(PhoenixKit.Users.Auth.Scope.t() | nil, kind()) :: boolean()

Whether scope may start a run of kind (jobs.manage and the kind's own permission against the active role) — what a screen asks before it offers a button that starts one.

cancel(scope, run)

Cancels a run. A batch already executing finishes first; its work is kept.

controls_for(scope, run)

The controls scope may offer on run right now: the state allows them, the kind offers them, and the person holds jobs.manage and the kind's own permission. For the Jobs page, which shows only what would work.

count_runs(filters \\ [])

@spec count_runs(keyword()) :: non_neg_integer()

How many runs match the filters of list_runs/1.

disable_system()

This function is deprecated. Jobs is always on.

Does nothing: Jobs is always on. Kept for callers of the module this used to be.

enable_system()

This function is deprecated. Jobs is always on.

Does nothing: Jobs is always on. Kept for callers of the module this used to be.

enabled?()

This function is deprecated. Jobs is always on.
@spec enabled?() :: boolean()

Always true: Jobs is core. Kept for callers of the module this used to be; there is no toggle.

get_config()

@spec get_config() :: map()

The Oban job counts by state and whether Jobs is on.

get_job_stats()

@spec get_job_stats() :: map()

Job statistics from the Oban jobs table (read through Oban's configured prefix; all zeros where no Oban instance runs in this VM, and a database error is raised, not hidden).

iex> PhoenixKit.Jobs.get_job_stats()
%{available: 5, scheduled: 2, executing: 1, completed: 100, ...}

get_run(uuid)

@spec get_run(String.t()) :: PhoenixKit.Jobs.Run.t() | nil

A run, or nil.

heartbeat(run)

@spec heartbeat(PhoenixKit.Jobs.Run.t()) :: :ok | {:error, :claim_lost}

Tells the engine a long batch is still alive, and checks that it still holds the run. Call between steps of a batch that may run for minutes. :ok, or {:error, :claim_lost} when something else holds the run now (stop working).

For a queued batch this is observability and a cooperative check only: the sweeper never reads it, and judges the batch by its own Oban job. For a script (run_inline/3) it renews the lease that keeps the sweeper from taking the run (PhoenixKit.Jobs.Run.inline_lease_seconds/0, an hour).

history(uuid)

The Activity entries of one run, oldest first.

kinds()

@spec kinds() :: [module()]

The kind modules known now (PhoenixKit.Jobs.Kinds.all/0).

list_runs(filters \\ [])

@spec list_runs(keyword()) :: [PhoenixKit.Jobs.Run.t()]

Runs, newest first. Filters (all optional): :module, :kind, :state (one or a list; :active for the unfinished ones), :scope (:site or {type, uuid}), :limit (default 25), :offset.

pause(scope, run)

Pauses a run (see the moduledoc for what a pause while a batch executes means).

resume(scope, run)

Resumes a paused run. Refused while the run is still pausing.

retry(scope, run)

@spec retry(PhoenixKit.Users.Auth.Scope.t(), PhoenixKit.Jobs.Run.t() | String.t()) ::
  {:ok, PhoenixKit.Jobs.Run.t(), :started | :existing} | {:error, term()}

Starts a new run like a failed or cancelled one (same kind, scope and args), recording which it retries. A finished run is never changed.

run_inline(kind, run_scope \\ :site, opts \\ [])

@spec run_inline(module(), run_scope(), keyword()) ::
  {:ok, PhoenixKit.Jobs.Run.t()} | {:error, term()}

Runs a run in the calling process to its end (or until it is paused or cancelled by someone else): what a Mix task or a script does instead of queueing. It goes through the same claim, checkpoint and failure rules as a queued run, and refuses a run a batch holds ({:error, :claimed}). Mode defaults to "script"; no Oban job is made. :on_progress is called with the run after every batch that made progress (a Mix task prints from it). A {:snooze, s} or a batch's schedule_in: is waited out — in short slices, so a pause or a cancel ends the wait.

The claim a script takes is a lease (PhoenixKit.Jobs.Run.inline_lease_seconds/0, an hour, renewed by heartbeat/1): the sweeper has no Oban job to ask about a script and leaves its claim alone while the lease stands. A script that dies leaves its run held until the lease runs out, when the sweeper takes the run back (and a new script may take it over). Like PhoenixKit.Jobs.System, this is trusted application code: it checks no permission.

start(scope, kind, run_scope \\ :site, opts \\ [])

@spec start(PhoenixKit.Users.Auth.Scope.t(), kind(), run_scope(), keyword()) ::
  {:ok, PhoenixKit.Jobs.Run.t(), :started | :existing} | {:error, term()}

Starts a run of kind for run_scope on behalf of the person behind scope. Options: :args. Returns {:ok, run, :started | :existing} or an error.

{:error, :raced} means the start met a run that finished at that very moment three times over: the trigger was not recorded — try again. {:error, :in_transaction} means it was called inside a transaction of the caller's, which would let a broadcast or on_finish/2 escape a rollback: call it after the transaction commits (see PhoenixKit.Jobs.Engine).