neosourceDocs
Search docs

Cancel a workflow run

POST/api/workflow-runs/{run_id}/cancel

cancelWorkflowRun

Cancels a non-terminal run. Jobs that never started go straight to `cancelled`; in-flight jobs transition to `cancelling` — the executing runner learns of the cancel on its next heartbeat, tears the job down, and reports it `cancelled`, at which point the run finalizes. Requires write access on the run's repo. 409 when the run is already finished.

Authentication is not described for this operation in the spec — that does not mean it is public. Check tokens and scopes.

curl

curl -X POST 'https://neosource.dev/api/workflow-runs/RUN_ID/cancel'

fetch

fetch("https://neosource.dev/api/workflow-runs/RUN_ID/cancel", {
  method: "POST",
});

Path parameters

run_idrequired

string

Responses

200Run cancelling/cancelled

application/json

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

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"

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

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"

test_summary
one of
  • null

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

    passedrequired

    integerint32

    skippedrequired

    integerint32

    totalrequired

    integerint32

toolchain_manifest
one of
  • null

  • 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 propertiesResolvedToolchain
    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

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

403Forbidden — one of: forbidden, needs_scope

application/json

one of
  • ErrorForbidden
  • NeedsScopeError

    object

    `403` body returned when listing private repos but the linked identity lacks the required provider scope. The SPA turns this into an incremental-authorization prompt (Tier 2) that calls the `/elevate` OAuth endpoint with this `scope`.

    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
  • 409Conflict — one of: already_exists, conflict
  • 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.

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.

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

    string

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

    "strip_one""flat"

    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
    • 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
    • object

      kindrequired

      string

      "store_path"

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.

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.