neosourceDocs
Search docs

List a repository's workflow runs (newest first)

GET/api/repos/{owner}/{repo}/workflow-runs

listRepositoryWorkflowRuns

Keyset-paginated, newest-first. Pass `next_cursor` from a previous page as `cursor` to fetch older runs. All filters are optional and combine conjunctively.

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/repos/OWNER/REPO/workflow-runs' \
  -H 'Authorization: Bearer $NEOSOURCE_TOKEN'

fetch

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

neo

neo actions list OWNER/REPO

Path parameters

ownerrequired

Repository owner or organization slug.

string

reporequired

Repository name.

string

Query parameters

workflow_id

Only runs of this workflow (UUID).

string

status

Only runs with this status (`queued`, `running`, …).

WorkflowRunStatus

string

Lifecycle of a workflow run. Mirrors the GHA-shaped surface that the SPA renders. The serde `snake_case` strings are the wire vocabulary the API emits (pinned by the `enum_wire_consistency` tests).

"pending_approval""queued""quota_blocked""concurrency_blocked""running""cancelling""succeeded""failed""cancelled"

run_number

Only the run with this per-workflow ordinal.

integerint64

trigger_commit

Only runs whose triggering event pointed at this exact commit (40-char git hex). How `neo push --runner` finds its push's runs.

string

cursor

Opaque pagination cursor from a previous response's `next_cursor`.

string

limit

Page size; default 30, max 100.

integerint64

Responses

200Runs page

application/json

WorkflowRunListResponse

object

Paginated run listing (`GET /api/repos/{owner}/{repo}/workflow-runs`). Newest-first; `next_cursor` is an opaque token to pass back as `cursor` for the next (older) page, `null` when this is the last page.

next_cursor

string | null

runsrequired

array

items

WorkflowRunResponse

object

actor_handle

string | null

Handle of the account whose action triggered the run (the pusher, the PR author, the manual dispatcher). `null` for actor-less causes (schedule) and on write receipts that skip the lookup (enqueue).

attemptrequired

integerint32

Re-run attempt (GHA's `github.run_attempt`), starting at 1. A re-run creates a new run that keeps `run_number` and increments this.

branch

string | null

The bookmark (branch) a `push` or ref-aware manual run targeted; `null` for other events and legacy manual rows.

commit_summary

string | null

First line of the trigger commit's message, when that commit is in the metadata projection. Populated on list/detail reads; `null` on write receipts and causes without a pinned commit.

enqueued_atrequired

integerint64

eventrequired
WorkflowRunEvent
finished_at

integer | nullint64

idrequired

string

previous_attempt_id

string | null

The run this one is a re-run of (UUID hex), or `null` for a first attempt. Lets the UI link to the previous attempt.

pull_request_number

integer | nullint32

The pull request number for `pull_request` runs.

repo_idrequired

string

run_numberrequired

integerint64

Per-workflow monotonic ordinal (GHA's `github.run_number`). Run 1 of a workflow is 1, the next is 2, etc. Surfaced to steps and used by the runner to populate `${{ github.run_number }}`. Re-run attempts of one logical run share this number.

started_at

integer | nullint64

statusrequired
WorkflowRunStatus
test_summary
toolchain_manifest
trigger_commit

string | null

Exact commit (40-char git hex) the run executes: push tip, PR head, or the CAS-consistent branch target of a ref-aware manual dispatch. `null` only for legacy manual rows and causes without a commit target (such as schedules). `neo push --runner` polls on this to find a push's runs.

workflow_idrequired

string

Standard errors

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

  • 400Bad Request — one of: invalid_input
  • 403Forbidden — one of: forbidden
  • 404Not Found — one of: not_found
  • 423Locked — one of: busy
  • 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.

WorkflowRunEvent

string

A [`WorkflowRunCause`] with its payload stripped — the closed event vocabulary run list rows surface (`WorkflowRunResponse.event`). The serde `snake_case` strings are the wire vocabulary (pinned by the `enum_wire_consistency` tests).

"push""change""manual""schedule""pull_request""merge_group"

WorkflowRunStatus

string

Lifecycle of a workflow run. Mirrors the GHA-shaped surface that the SPA renders. The serde `snake_case` strings are the wire vocabulary the API emits (pinned by the `enum_wire_consistency` tests).

"pending_approval""queued""quota_blocked""concurrency_blocked""running""cancelling""succeeded""failed""cancelled"

WorkflowRunTestSummary

object

Per-run test results parsed from a `JUnit` report artifact at run completion. Counts are individual testcases.

failedrequired

integerint32

failuresrequired

array

First N failing tests (server caps the stored list).

items

WorkflowRunTestFailure

object

One failing test out of the run's `JUnit` report.

message

string | null

The failure/error message attribute, when present.

namerequired

string

Test identity: `classname::name` when the report carries a classname, else the bare testcase name.

passedrequired

integerint32

skippedrequired

integerint32

totalrequired

integerint32

ToolchainManifest

object

Everything one run resolved, keyed by [`ToolchainKey::storage_key`]. Written once per run and read by every later reader — the second matrix leg, a re-run, a replay. Authoritative over the constraint that produced it: a re-run re-materializes these artifact identities rather than asking the catalog again (ADR 0083 §2).

catalog_version

string | null

sha256 of the catalog file that produced these entries. **Provenance only, never a lookup key.** The catalog is compiled into the binary, so a server running catalog C′ cannot evaluate against pinned catalog C — the rows are not in the process. Pinning a hash you cannot look up is a promise the deployment model cannot keep, which is why authority sits on the OUTPUT (ADR 0083 §2).

entries

object

`storage_key` → what it resolved to.

additional properties

ResolvedToolchain

object

What the catalog answered, and what a re-run re-materializes.

artifactrequired

ToolArtifact

Where a resolved toolchain's bytes come from.

one of
  • object

    A bare nixpkgs attribute at a pinned revision. One row serves every system; the ref may differ per version row, which is how one platform serves Node 20 and Node 26 at once (ADR 0075 dynamic routing is what makes that affordable).

    attrrequired

    string

    kindrequired

    string

    "nix"

    nixpkgs_refrequired

    string

  • object

    An upstream archive, fetched once platform-side, verified against an upstream-published checksum and stored content-addressed. The digest is the identity; the URL is provenance.

    kindrequired

    string

    "dist"

    layoutrequired
    DistLayout
    sha256required

    string

    urlrequired

    string

  • object

    A flake ref or a bare attribute with no catalog row — ADR 0057's escape hatch. Recorded as a raw installable rather than a tool version, because that is honestly all we know about it.

    installablerequired

    string

    kindrequired

    string

    "passthrough"

  • A bare nixpkgs store path (ADR 0101 §1). Materializing it is SUBSTITUTION, not evaluation: no tree fetch, no eval, no `[refs]` entry, no fleet rollout. 289,521 pairs cost the resolver what 2 cost. A validated payload rather than three loose fields: both of its invariants are enforced by [`StorePathArtifact`], on construction and on deserialize alike. The wire form is unchanged — serde flattens an internally-tagged newtype variant — so this is still `{"kind":"store_path","store_path":…,"outputs":[…],…}`.

    all of
toolrequired

string

Canonical tool name (aliases resolve to it), or the raw name for a passthrough installable.

variant

string | null

Distribution/vendor where the tool has one (`temurin`, `zulu`, …).

versionrequired

string

The concrete version. **Open vocabulary, deliberately `String`** — the set of version strings is unbounded by construction, so this is the documented exception to the closed-set-DTO-enum rule.

unreadable

object

`storage_key` → why a tolerant reader could not interpret that entry. **Populated only by a reader, never by the server.** The server writes what it can serialize, by construction, so this is empty in every row of `workflow_runs.toolchain_manifest`. A reader fills it in when it meets a [`ToolArtifact`] kind it predates and sets that one entry aside rather than failing the whole payload. A missing key and an unreadable key are NOT the same state: missing takes the late-bound resolve route and, failing that, an approximating local table; unreadable must REFUSE at the point of use. A job that never asks for an unreadable key runs untouched — that is what makes setting one entry aside safe at all. Additive on the wire (`default`, and skipped when empty), so an older reader ignores it and the common empty case costs nothing.

additional properties

string

DistLayout

string

How a dist archive unpacks into the tool-cache root.

"strip_one""flat"

StorePathArtifact

object

The `store_path` artifact's payload, with its invariants enforced. Two of them, and neither was a type before: 1. **`store_path` is a well-formed store path UNDER THE REAL STORE.** It deserializes straight out of `workflow_runs.toolchain_manifest` jsonb and [`ToolchainManifest::has_store_path`] is the agent's whole authorization surface, so as a raw `String` this answered "recorded" for anything that byte-matched a recorded entry — and left the grammar as the daemon's job alone rather than a property of the value. It takes BOTH halves to be that claim, and only the first was a type before: [`NixStorePath`] carries the GRAMMAR and is root-agnostic on purpose (the daemon's root is env-configurable and its own tests point it at a directory of their own), so `/etc/<hash>-passwd` is a shaped path and that type accepts it by design. [`StorePathArtifact::new`] pins the ROOT to [`NixStoreRoot::default`], on construction and on deserialize alike, which is what makes a recorded artifact naming anything outside the real store unconstructable. 2. **`bin_output` is one of `outputs`.** The `deno` → `denort` lesson is that which output carries `bin/` is index-ANSWERED, never guessed, and an answer naming an output the derivation did not have is not an answer. That was prose in a doc comment; it is checked here. `revision` stays an unvalidated `String` on purpose, and the asymmetry is the point: it is provenance only. No decision is made on it — that is exactly why [`ToolArtifact::nixpkgs_ref`] does not return it — so a malformed revision misleads a reader without routing anything, while a malformed store path is an authorization answer.

bin_outputrequired

string

Which output `store_path` is. Index-answered, never guessed — the `deno` → `denort` lesson says a name heuristic is not enough. A member of `outputs` by construction.

outputsrequired

array

Every output the derivation had, for provenance only.

items

string

revisionrequired

string

The revision the index chose (ADR 0101 §2a records it "beside the store path"). Deliberately NOT named `nixpkgs_ref` and deliberately NOT returned by [`ToolArtifact::nixpkgs_ref`]: that accessor drives `group_by_ref` and the daemon's attr path on 8080, and a revision leaking there routes a ref that is not in `ALLOWED_REFS` — the 2026-07-31 403 storm.

store_pathrequired

NixStorePath

string

An absolute, well-formed store path — `<root>/<hash>-<name>` for SOME valid [`NixStoreRoot`] — validated on construction AND on deserialize. **The grammar is a property of the VALUE, not a step some caller remembers.** `ToolArtifact::StorePath` is deserialized straight out of `workflow_runs.toolchain_manifest` jsonb and is the agent's whole authorization surface (`ToolchainManifest::has_store_path`), so a raw `String` there would let `/etc/passwd` answer "recorded" the moment it byte-matched a recorded entry, and would leave the grammar as the daemon's job alone. Validating in `Deserialize` too is the half that matters: an invariant that holds only for freshly-built values is not an invariant, and every value on this path arrives from a database. Deliberately ROOT-INDEPENDENT: whether a path is under the root *this* process serves is [`store_path_leaf`]'s question, and that root is env-configurable. This type answers the other one — is it shaped like a store path at all. Which means the grammar alone is NOT the authorization answer, and the doc must not be read as claiming it is: `/etc` is a root [`NixStoreRoot`] admits, so `/etc/<hash>-passwd` is a well-formed value of this type. Pinning a RECORDED artifact to the real store is [`crate::model::toolchain::StorePathArtifact::new`]'s clause, one layer up, where the recording happens.