Anatomy of a Workflow File

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:

The header and the first job of .github/workflows/fundamentals.ymlYAML
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 1

run-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.