GitHub Actions Workflow Syntax: Events, Jobs, Steps, Runners and Expressions

Every GitHub Actions pipeline is a YAML file. In this lesson you will read a complete annotated workflow and learn each part: events, jobs, steps, runners, expressions, conditions, outputs, concurrency and permissions.

Advertisement

๐Ÿ“– What You’ll Learn: the building blocks of a workflow, a complete annotated example, events and filters, job keys like needs, if and timeout, steps with uses and run, expressions and contexts, status functions, passing outputs between steps and jobs, and runners and cost.


๐Ÿงฉ Anatomy of a Workflow

A workflow is a YAML file in .github/workflows/ that GitHub Actions runs when an event happens. The building blocks:

PartMeaning
WorkflowOne YAML file; a repository can have many
Event (on)What starts it: push, pull request, schedule, manual run…
JobA set of steps that runs on one runner (virtual machine); jobs run in parallel by default
StepOne command (run) or one reusable action (uses)
RunnerThe machine: GitHub-hosted (Ubuntu, Windows, macOS) or self-hosted
ActionA reusable unit from the Marketplace or your own repository

If you are completely new, read GitHub Actions Fundamentals for Beginners first.


๐Ÿ“„ A Complete Annotated Workflow

name: CI                                   # shown in the Actions tab
run-name: CI for ${{ github.ref_name }} by @${{ github.actor }}

on:
  push:
    branches: [ main ]
    paths-ignore: [ '**.md' ]              # skip runs for documentation-only changes
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '30 20 * * *'                  # every day at 20:30 UTC
  workflow_dispatch:                       # manual "Run workflow" button
    inputs:
      browser:
        description: 'Browser project to run'
        type: choice
        options: [ chromium, firefox, webkit ]
        default: chromium

permissions:
  contents: read                           # least privilege for GITHUB_TOKEN

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true                 # cancel older runs of the same branch

env:
  NODE_VERSION: 22

jobs:
  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
      - run: npm ci
      - run: npm run lint

  test:
    needs: lint                            # runs after lint succeeds
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
      - run: npm ci
      - run: npx playwright install --with-deps
      - name: Run tests
        run: npx playwright test --project=${{ inputs.browser || 'chromium' }}
      - name: Upload report
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/

โšก Events (on)

EventFires whenCommon filters
pushCommits are pushedbranches, tags, paths, paths-ignore
pull_requestA PR is opened, updated or reopenedbranches (the target branch), types
scheduleOn a cron timetable (UTC)cron expression
workflow_dispatchSomeone clicks Run workflow (or uses the API or gh workflow run)inputs
workflow_callAnother workflow calls this oneinputs, secrets (reusable workflows)
releaseA release is publishedtypes: [published]
workflow_runAnother workflow finishesworkflows, types

๐Ÿ’ก Tip: Use paths / paths-ignore so a README change does not trigger a 20-minute test run.


๐Ÿ—๏ธ Jobs

KeyPurpose
runs-onRunner: ubuntu-latest, windows-latest, macos-latest, or labels of self-hosted runners
needsDependencies: needs: [lint, build]
ifRun the job only when a condition is true
timeout-minutesStop runaway jobs (default is 360)
strategy.matrixRun the job for several combinations (matrix)
environmentDeploy to a protected environment (environments)
outputsValues passed to later jobs
container / servicesRun in a Docker image, or start databases alongside the job

๐Ÿชœ Steps

steps:
  - name: Check out code
    uses: actions/checkout@v4           # an action, with inputs under "with"
    with:
      fetch-depth: 0

  - name: Install
    run: npm ci                         # a shell command

  - name: Multi-line script
    working-directory: ./tests
    shell: bash
    run: |
      echo "Running on ${{ runner.os }}"
      ls -la

  - name: Optional step
    continue-on-error: true
    run: npm run audit

๐Ÿงฎ Expressions and Contexts

Expressions are written as ${{ ... }} and can read contexts:

ContextExamples
githubgithub.ref_name, github.sha, github.event_name, github.actor
env / vars / secretsEnvironment variables, configuration variables, secrets
inputsInputs from workflow_dispatch or workflow_call
matrixThe current matrix values
steps / needsOutputs of earlier steps or jobs
runnerrunner.os, runner.temp

Status functions for if: success() (default), failure(), always(), cancelled(). Other functions: contains(), startsWith(), format(), hashFiles(), toJSON().

- name: Notify on failure
  if: ${{ failure() && github.ref == 'refs/heads/main' }}
  run: echo "Main is broken!"

๐Ÿ”— Passing Data Between Steps and Jobs

jobs:
  version:
    runs-on: ubuntu-latest
    outputs:
      app_version: ${{ steps.read.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - id: read
        run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"

  report:
    needs: version
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing version ${{ needs.version.outputs.app_version }}"

Other special files: $GITHUB_ENV (set environment variables for later steps) and $GITHUB_STEP_SUMMARY (Markdown shown on the run’s summary page).


๐Ÿ’ฐ Runners and Cost

GitHub-hosted runners are free for public repositories. Private repositories get a monthly allowance of free minutes depending on the plan; Windows and macOS minutes count more than Linux. Use Linux runners, timeout-minutes, concurrency and paths filters to keep usage low. Self-hosted runners run on your own machines for special hardware or network access.


๐Ÿ“ Assignment

  1. In git-practice, create .github/workflows/ci.yml with push, pull_request and workflow_dispatch triggers.
  2. Add two jobs, lint and test (they can just echo), where test needs lint.
  3. Add a workflow_dispatch input and print it in a step.
  4. Add concurrency with cancel-in-progress and push twice quickly to see the first run cancelled.
  5. Pass a value from one job to another with outputs, and write a line to $GITHUB_STEP_SUMMARY.
  6. Add a nightly schedule and a paths-ignore for Markdown files.

โ“ FAQ

๐Ÿค” Where do GitHub Actions workflow files go?

In the .github/workflows/ folder of the repository, as .yml or .yaml files.

๐Ÿค” What is the difference between a job and a step?

A job runs on its own runner and contains steps. Steps run in order inside the job; jobs run in parallel unless connected with needs.

๐Ÿค” How do I run a workflow manually?

Add the workflow_dispatch trigger, then click Run workflow on the Actions tab or use gh workflow run.

๐Ÿค” How do I run a step only when a previous step fails?

Use if: ${{ failure() }} on the step.

๐Ÿค” What time zone does schedule cron use?

UTC. Convert your local time, for example 02:00 IST is 20:30 UTC on the previous day.


๐ŸŽฏ Next Steps

๐ŸŽฌ Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

1 thought on “GitHub Actions Workflow Syntax: Events, Jobs, Steps, Runners and Expressions”

Leave a Comment