GitHub Actions startup_failure
A GitHub Actions startup_failure is a conclusion on the run itself, recorded before any job was created, which is why there are no job logs to open. Either the workflow file did not compile on the ref that raised the event, something it references could not be resolved, or GitHub could not prepare the run.

What this error means
The run appears in the Actions tab and ends almost immediately. There is no job list, no step output and no annotation pointing at a line of your code, because no job was ever created and no runner was ever asked for one. The API reports the run with a status of completed and a conclusion of startup_failure. Anything you would normally reach for is missing: there is nothing to re-run at job level, nothing to SSH into, and no runner log, because the failure happened on GitHub's side of the handover.
"status": "completed",
"conclusion": "startup_failure"Why there is nothing to look at
A workflow run goes through a fixed sequence: the event is accepted, the workflow file is compiled on the ref that raised it, jobs are created, a runner is assigned, and steps run. startup_failure is the run stopping at the second step of that list, so everything to the right of it never happened.
That single fact removes most of the usual debugging. Changing runs-on, resizing the machine, moving to a different runner provider, adding a continue-on-error or retrying at job level all operate on jobs, and there are no jobs. It is also why this page carries no reproduction: the failure is not reproducible on a runner, because a runner is never involved.
It is common enough to be worth recognizing on sight. A GitHub issue search for "startup_failure" alongside "github actions" returned 4,509 results when we read it on 20 September 2026, and the pattern in those threads is consistent: a run with no logs, and a cause in the workflow file rather than in the job.
Common causes
The workflow file does not compile on the triggering ref
Invalid YAML, an unknown key, a bad expression, or a matrix that resolves to nothing. The compile happens against the ref that raised the event, which for a schedule is always the default branch and for pull_request_target is the base branch, not your feature branch.
A reusable workflow cannot be resolved
A deleted tag, a renamed path, a branch that no longer exists, or a private repository whose workflows are not shared with the caller. Resolution happens at compile time, so the failure has the shape of a file problem even though the file is fine.
The run cannot be created under the permissions it has
A missing scope in the permissions block of the calling workflow, or a rule that blocks creation. In our experience this is the cause that gets misdiagnosed as invalid YAML most often, because the file itself is well formed.
A transient error preparing the run
GitHub occasionally fails to prepare a run for reasons on its own side. This is the only cause where re-running is the right first move, and it is identified by the fact that it does not repeat.
How to fix it
Read the workflow at the ref that raised the event
- Identify the trigger, and from it the ref: a schedule and a manual dispatch use the default branch, a
pushuses the pushed ref. - Open the workflow file at that ref rather than in your working copy.
- If the file there is older than your fix, the fix has not merged yet, and nothing else on this page applies.
Lint workflows before they merge
The compiler is the only thing that will tell you the file is wrong, and it only runs after you push. A linter in a pull request check moves that feedback earlier and costs one job.
jobs:
lint-workflows:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: |
bash <(curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint -colorPin reusable workflows to refs that will keep existing
Point uses: at a tag or a commit you control rather than at a moving branch, and confirm the calling repository can see the called one. A private repository has to share its workflows explicitly before another repository can call them.
# fragile: the branch may be deleted or renamed
uses: my-org/ci-workflows/.github/workflows/build.yml@feature/new-build
# durable
uses: my-org/ci-workflows/.github/workflows/build.yml@v3Grant the scope the run needs, one scope at a time
Start from the least access that lets the run be created rather than from write-all. If a called workflow needs to write statuses or checks, name that scope in the caller and leave the rest read-only.
permissions:
contents: read
checks: write
statuses: writeThe file is compiled from the triggering ref, not your branch
This is the cause people lose the most time to. GitHub compiles the workflow from the ref that raised the event, and for several common triggers that is the default branch rather than the branch you are working on. A scheduled run always reads the file from the default branch, a pull_request_target reads it from the base branch, and a workflow_dispatch compiles the ref you dispatch, so a fix that is green in your pull request often has not taken effect yet.
The practical check is to open the workflow file at that exact ref and read it there, rather than reading the copy in your editor. A tab where a space belongs, a duplicated key, a broken expression, or a strategy block indented one level too far will all fail the compile, and the copy on your branch may already have the fix.
The second practical check is a linter, because the compiler is stricter than a YAML parser. A file can be valid YAML and an invalid workflow: an unknown key, an expression referencing a context that does not exist at that point, or a matrix that produces no combinations.
# read the file as the event will read it
gh api repos/{owner}/{repo}/contents/.github/workflows/ci.yml?ref=main \
--jq .content | base64 -d | head -40
# then lint it
actionlint .github/workflows/ci.ymlWhat the run references has to resolve too
A reusable workflow is resolved while the run is being compiled, before any job exists, so anything that stops that resolution ends the run here. A uses: pointing at a tag that was deleted, a path that was renamed, a branch that no longer exists, or a private repository that has not shared its workflows with the calling repository all land in the same place.
Permissions belong in this group as well. When the calling workflow does not hold the access the run needs at creation time, the run cannot be created, and the symptom is the same empty run. Issue threads fixing this failure frequently do it by granting a single scope in the permissions block, which is worth trying before assuming the YAML is malformed.
The same applies to required approvals and to environments: if the run cannot be constructed under the rules that apply to it, the conclusion is recorded on the run.
jobs:
call:
permissions:
contents: read
statuses: write # the scope the called workflow needs
uses: my-org/ci-workflows/.github/workflows/build.yml@v3 # a ref that still existsWhen the file is fine
A small share of these are GitHub-side. If the same commit produced a green run an hour earlier and nothing about the file or its references changed, re-run once and check the status page rather than editing the workflow.
The test is whether it repeats. A transient failure clears on the next attempt; a compile error does not, because the same file on the same ref compiles the same way every time. If a re-run fails identically, stop treating it as flaky and go back to the file.
How to prevent it
- Run a workflow linter on every pull request that touches
.github/workflows. - Pin reusable workflows to tags or commits, not to branches.
- Keep the
permissionsblock explicit so a missing scope is visible in review. - Test schedule and dispatch workflows on the default branch, since that is where they compile.
Frequently asked questions
Why does my workflow show startup_failure with no logs?
Which branch is the workflow file read from?
push uses the pushed ref, a schedule always uses the default branch, and a pull_request_target uses the base branch. A workflow_dispatch compiles the ref you dispatch; the default branch only decides whether the workflow is offered at all. That is why a fix on a feature branch changes nothing for a scheduled run until it merges.Can a runner problem cause startup_failure?
runs-on, moving to a larger machine or switching to a different runner provider cannot affect it, and a runner that is offline or busy produces a queued job instead, which is a different failure with different symptoms.