# `PhoenixKit.Jobs.Run`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.55.1/lib/phoenix_kit/jobs/run.ex#L1)

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 becomes `paused` / `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.

# `t`

```elixir
@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()
}
```

# `active?`

```elixir
@spec active?(t() | String.t()) :: boolean()
```

Whether the run has not finished.

# `active_states`

The states of an unfinished run: at most one per kind and scope.

# `claimed?`

```elixir
@spec claimed?(t()) :: boolean()
```

Whether a batch holds the run right now.

# `inline_claim?`

```elixir
@spec inline_claim?(t()) :: boolean()
```

Whether a script (`run_inline/3`) holds the run, not an Oban batch.

# `inline_lease_seconds`

Seconds a script's claim stands without a heartbeat.

# `inline_waiting?`

```elixir
@spec inline_waiting?(t()) :: boolean()
```

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).

# `insert_changeset`

The changeset a new run is inserted with.

# `lease_expired?`

```elixir
@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.

# `modes`

The ways a run can have been started.

# `percent`

```elixir
@spec percent(t()) :: non_neg_integer() | nil
```

A whole number between 0 and 100, or nil when the total is not known.

# `scope`

```elixir
@spec scope(t()) :: :site | {String.t(), String.t()}
```

The run's scope as `{type, uuid}`, or `:site` when it is about nothing in
particular.

# `seconds_until_wake`

```elixir
@spec seconds_until_wake(t(), DateTime.t()) :: non_neg_integer()
```

Seconds until `wake_at`, never negative; 0 when the run has no delay pending.

# `states`

Every state a run can be in.

# `terminal?`

```elixir
@spec terminal?(t() | String.t()) :: boolean()
```

Whether the run has finished, one way or another.

# `terminal_states`

The states a run never leaves.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
