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.