A workflow needs on (its triggers, Triggers) and jobs; each job needs runs-on (a runner label, Runner Images) and steps. This is the top of fundamentals.yml with its first job:
name: Fundamentals
run-name: Fundamentals on ${{ github.event_name }} by @${{ github.actor }}
# the on: block of Section 3.9.2 goes here
permissions:
contents: read
jobs:
syntax:
runs-on: ubuntu-24.04
outputs:
checked: ${{ steps.check.outputs.checked }}
steps:
- uses: actions/checkout@v7
- id: check
name: Parse every JavaScript file
run: |
files=$(git ls-files '*.js')
for f in $files; do node --check "$f"; done
echo "checked=$(echo $files | wc -w)" >> "$GITHUB_OUTPUT"
- if: inputs.break-syntax
run: exit 1run-name titles each run. A step either uses: an action (actions/checkout clones the commit being built; @v7 is a tag its publisher moves to each 7.x release) or run:s a script, and its id lets later expressions read its outputs. Declare permissions in every workflow: unlisted scopes become none, whereas the default is read and write to almost everything on many repositories created before February 2023.
YAML itself failed the first run of pull request #15: in an unquoted value such as run: echo "PR #${{ ... }}", a space and # start a comment, so bash got half a line. A block scalar (run: |) keeps every character.