What this checks
Paste a workflow file and it's checked for the mistakes that only surface once GitHub actually tries to run it: a missing on trigger or jobs mapping, a job with neither runs-on nor uses, a step with both uses and run (or neither), and duplicate step ids within a job.
It also cross-references the parts of a workflow that have to agree with each other but aren't checked by YAML syntax alone: a needs: entry pointing at a job that doesn't exist, a circular needs chain, and ${{ needs.<job>.outputs.* }} or ${{ steps.<id>.outputs.* }} expressions referencing a job that isn't listed in needs: or a step id that was never declared — both compile fine as YAML and both silently produce an empty string at runtime instead of failing loudly.
Third-party uses: references are checked for a missing @ref (required syntax, not optional) and flagged — as a warning, not an error — when pinned to a branch or tag rather than a full commit SHA, since a tag can be moved or a branch can change out from under you but a commit SHA can't.
What it doesn't do
This is a structural and cross-reference check, not a full reimplementation of GitHub's workflow schema. It doesn't evaluate if: expressions, resolve reusable workflow inputs/outputs, or verify that a referenced action or reusable workflow actually exists on GitHub. It also doesn't check for security or best-practice issues like script-injection risk or the pull_request_target "pwn request" pattern — for those, use GitHub Actions Linter.
FAQ
Why is an unpinned action a warning instead of an error?
Pinning to @v4 instead of a commit SHA is extremely common and works fine for most projects — it's a supply-chain hardening recommendation, not something that breaks the workflow. A completely missing @ref, on the other hand, is invalid syntax GitHub can't resolve at all, so that stays an error.
Why flag needs.X.outputs when X isn't in needs:?
GitHub only guarantees a job has started (and its outputs exist) if it's listed in the referencing job's needs:. Referencing another job's output without depending on it is a common copy-paste mistake — the expression evaluates to an empty string instead of erroring, so it fails silently at runtime rather than at parse time.
Want more examples like these?
See Common GitHub Actions Errors for these plus the ::set-output:: command that's silently done nothing since 2023, and the security-shaped mistakes GitHub Actions Linter catches instead.