Docs
Search docs

List a run's jobs with their step records

GET/api/workflow-runs/{run_id}/jobs

listWorkflowRunJobs

Authentication optional — this operation also accepts anonymous requests. When authenticating, use a bearer token or a session cookie.

curl

curl -X GET 'https://neosource.dev/api/workflow-runs/RUN_ID/jobs' \
  -H 'Authorization: Bearer $NEOSOURCE_TOKEN'

fetch

fetch("https://neosource.dev/api/workflow-runs/RUN_ID/jobs", {
  method: "GET",
  headers: {
    Authorization: "Bearer $NEOSOURCE_TOKEN",
  },
});

Path parameters

run_idrequired

string

Responses

200Jobs with embedded steps

application/json

WorkflowRunJobListResponse

object

Jobs (with embedded steps) of one run (`GET /api/workflow-runs/{run_id}/jobs`).

jobsrequired

array

items

WorkflowRunJobResponse

object

cache_transfer
carried_from
compile_cache
definitionrequired

object

Definition slice the runner needs to execute. JSON shape mirrors `neosource_core::model::WorkflowJobDef`.

enqueued_atrequired

integerint64

executed_by_runner

string | null

The runner that claimed this job (UUID hex), materialized at claim like `executed_on`. `null` until first claim, on carried jobs, and ALWAYS when `executed_on` is `actor` (a stable id would link one person's machine across public runs).

executed_on
executed_runner_name

string | null

Display name of the runner that executed this job, as it was at claim (a later rename does not rewrite history). Set ONLY when `executed_on` is `workspace`: a personal runner's name defaults to its hostname and run pages can be public, so an actor-bound execution is shown as "ran on @handle's machine" instead, and a hosted one as "hosted". Also `null` for a runner that never reported a name.

exit_code

integer | nullint32

finished_at

integer | nullint64

idrequired

string

job_keyrequired

string

log_offsetrequired

integerint64

log_truncated

boolean

The job's log hit its per-job size cap (ADR 0029 §"Log size per job / retention"). The log ends with a server-authored truncation marker line and nothing more will ever be appended. This flag, not that line, is the authoritative signal — a job can echo the same text.

matrix_values

object | null

Matrix combination, when this job row came from matrix expansion.

needsrequired

array

Other jobs that must succeed before this one is eligible.

items

string

pod_scheduled_at

integer | nullint64

When this job's pod was scheduled onto a node (epoch ms; Kubernetes reports it at 1 s precision). `null` until the runner reports it, for a job that did not run on a k8s pod, and for every job that ran before the field existed. `started_at` → this is time spent waiting for capacity.

pod_started_at

integer | nullint64

When this job's runner container started (epoch ms; 1 s precision). `pod_scheduled_at` → this is the pod's boot (image pull, Nix store, sandbox). Same `null` cases as `pod_scheduled_at`.

repo_idrequired

string

run_idrequired

string

runs_on

array

Labels the job's `runs-on:` requires (a stored legacy `runs-on: nix` job reports `["ubuntu-latest"]`, ADR 0174).

items

string

started_at

integer | nullint64

statusrequired
WorkflowRunJobStatus
steps

array

Per-step records reported by the runner. Empty until the runner posts its first snapshot (and always empty on the claim payload).

403Forbidden — one of: forbidden, needs_scope

application/json

one of
  • ErrorForbidden
  • NeedsScopeError

    object

    `403` body returned when a classic OAuth token is asked for something its granted scopes do not cover — an explicit private listing without `repo`, or the one-click GitHub mirror without `admin:public_key`. `scope` names the missing grant so the client can say exactly what the token lacks. There is no elevation route to widen it any more (a re-link goes through the GitHub App, whose tokens carry no scopes), so the client's answer is to say so, never to start a handshake.

    errorrequired

    string

    Always `needs_scope` — this body exists to carry the extra fields that kind needs.

    "needs_scope"

    scoperequired

    string

    The provider scope to request via elevation (e.g. `"repo"`).

Standard errors

Bodies documented once for the whole API — see standard errors.

  • 400Bad Request — one of: invalid_input
  • 404Not Found — one of: not_found
  • 429Rate limited — retry after the `Retry-After` header
  • 500Internal server error
  • 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
  • 504Gateway timeout — the request exceeded the server's handling budget

Schemas

Referenced above. Listed here rather than expanded inline, so the same definition is not repeated at every level.

JobCacheTransferResponse

object

A job's cache transfer, measured by the server as it served the job's cache requests (`workflow_run_job_cache_transfer`). Every byte figure is COMPRESSED object bytes as the server sent or stored them (the actions/cache tarball, the build-cache pool object), never the unpacked size. A download counts when the server builds a hit response, at the entry's full size, even if the client aborts mid-body; a `lookup` or HEAD never counts. An upload counts only once durably stored. Every request that reaches the server counts once (a repeated pool PUT counts each time). Telemetry, not accounting: a crashed API node loses up to 30 s of counts, and nothing is ever counted twice.

native_restored_bytes

integer | nullint64

Bytes those restores sent. `null` exactly when `native_restores` is.

native_restores

integer | nullint64

actions/cache restore hits served to this job. `null` = not measured: the job's row predates restore tracking, so it may have restored. A job that ran during the deploy introducing this count may under-count (a restore served by a not-yet-updated node is missed).

native_save_skippedrequired

integerint64

actions/cache saves refused because the key already existed (keys are immutable); they stored nothing.

native_saved_bytesrequired

integerint64

native_savesrequired

integerint64

actions/cache saves that stored a new entry.

pool_bytes_downrequired

integerint64

Bytes the pool's hits sent.

pool_bytes_uprequired

integerint64

pool_getsrequired

integerint64

Build-cache pool GETs (a miss counts with 0 bytes).

pool_hitsrequired

integerint64

pool_putsrequired

integerint64

Build-cache pool PUTs that stored, and their bytes.

CarriedFromResponse

object

Where a carried job ([`WorkflowRunJobResponse::carried_from`]) executed.

attemptrequired

integerint32

That run's `attempt`.

job_idrequired

string

The job that executed (UUID hex).

run_idrequired

string

Its run — an earlier attempt of this run (UUID hex).

CompileCacheResponse

object

A job's compile-cache result, as SERVED to a reader: the raw counters plus the two figures derived from them. The derived fields are computed at read time rather than stored, so improving the model never means a migration and the inputs stay visible beside the output. Both are `null` where they have no meaning — notably `estimated_saved_ms` on a job that hit on everything and therefore never measured a compile.

compilationsrequired

integerint64

compile_msrequired

integerint64

estimated_saved_ms

integer | nullint64

Modelled ms the cache saved: `hits × (mean compile − mean cache read)`. `null` when the terms do not exist — see `JobCompileCache`.

hit_rate

number | nulldouble

Hits ÷ (hits + misses), or `null` when there were no requests.

hitsrequired

integerint64

missesrequired

integerint64

read_hit_msrequired

integerint64

requestsrequired

integerint64

RunnerBindingKind

string

Which trust binding a job executed under (ADR 0056 §1/§7). Serialized on the wire and in `workflow_run_jobs.executed_on` as the `snake_case` strings `hosted` / `workspace` / `actor`.

"hosted""workspace""actor"

WorkflowRunJobStatus

string

Lifecycle of an individual job inside a run. Distinct from the run-level status because `needs:` ordering means some jobs are `Waiting` while others are `Running`. The serde `snake_case` strings are the wire vocabulary the API emits (pinned by the `enum_wire_consistency` tests).

"waiting""ready""running""cancelling""succeeded""failed""cancelled""skipped"

WorkflowRunStepResponse

object

One step of a job, reported by the executing runner. Offsets index into the job's log byte stream (`GET /api/jobs/{job_id}/logs`), so the UI can slice the job log per step. `log_end_offset` is `null` while the step is in flight.

cache
one of
  • StepCacheReportDto

    object

    What a step's cache RESTORE got back. `null` on the step it belongs to means "this step said nothing about caching" — never "miss". Every step that predates the feature, and every step that is not a cache restore, reads null, so a reader that renders absence as a miss libels most of the platform's history.

    bytes

    integer | nullint64

    Compressed size of the restored entry, when the transport knew one.

    matched_key

    string | null

    The key that MATCHED — the primary key on an exact hit, the restore-key-matched entry's key on a partial one. `null` on a miss.

    outcomerequired

    StepCacheOutcome

    string

    What a cache RESTORE got back (ADR 0016; phase 2 of `plans/ci-cache-and-step-visibility-2026-08.md`). Scoped to restores on purpose. A restore is one-to-one with a step, so the fact has an unambiguous owner and an unambiguous meaning — "did the cache help here". A SAVE is not: the combined `actions/cache@v4` saves from a job-end post-hook that runs after every step has finished, so attributing it to a step index would mean inventing one. Adding a save outcome later is additive to this enum and to the column that stores it.

    "exact""partial""miss"

  • null

finished_at

integer | nullint64

log_end_offset

integer | nullint64

log_start_offsetrequired

integerint64

namerequired

string

started_at

integer | nullint64

statusrequired

WorkflowRunStepStatus

string

Lifecycle of one step inside a job. Written by the runner (it knows the boundaries exactly) via an idempotent full-snapshot upsert; never derived server-side from log parsing.

"queued""running""succeeded""failed""skipped"

step_indexrequired

integerint32