Skip to content
8 min read·Lesson 2 of 8

Workflow Syntax: Jobs, Steps, and Triggers

Master the YAML grammar of GitHub Actions: triggers (on:), jobs, steps, expressions, contexts, conditionals, and job dependencies.

GitHub Actions workflows are written in YAML. The schema is small but expressive — every advanced feature builds on these primitives. This lesson covers every element you will use day-to-day.

The Top-Level Keys

KeyPurpose
nameDisplay name in the Actions UI
onTriggers that start the workflow
permissionsGITHUB_TOKEN scopes for the workflow
envWorkflow-wide environment variables
concurrencyCancel/queue policies for concurrent runs
jobsThe work to do

Triggers (on:)

Common event triggers

on:
  push:
    branches: [main, 'release/**']
    paths-ignore: ['**.md']
  pull_request:
    types: [opened, synchronize, reopened]
  schedule:
    - cron: '0 6 * * *'   # daily at 06:00 UTC
  workflow_dispatch:        # manual button in the UI
    inputs:
      environment:
        type: choice
        options: [staging, production]
        default: staging
  release:
    types: [published]

Repository, organization, and external events

GitHub Actions supports 35+ event types. The most common beyond push/PR are workflow_dispatch (manual trigger with inputs), workflow_call (called by another workflow — the foundation of reusable workflows), and repository_dispatch (triggered by external API calls).

Jobs

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

  build:
    needs: test                  # only runs if 'test' succeeds
    runs-on: ubuntu-latest
    steps:
      - run: npm run build

  deploy:
    needs: [test, build]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

Jobs default to parallel execution. Use needs: to declare dependencies and create a directed acyclic graph (DAG) of execution.

Job-level settings

  • runs-on — runner label (ubuntu-latest, windows-latest, macos-latest, or self-hosted labels)
  • timeout-minutes — defaults to 360; useful to cap runaway builds
  • strategy.matrix — fan-out variant builds (covered in lesson 6)
  • environment — link a job to a deployment environment for approvals/secrets
  • defaults — set default shell, working-directory
  • container — run all steps inside a Docker image
  • services — sidecar containers (e.g., Postgres for tests)

Steps

Two step types:

steps:
  - name: Run a shell command
    run: |
      echo "Hello"
      npm ci

  - name: Use a pre-built Action
    uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: 'npm'

Inside one job, steps share the same runner filesystem. They do not share environment-variable mutations across steps unless you write them to $GITHUB_ENV:

- name: Compute and export a variable
  run: echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Use it in the next step
  run: echo "Short SHA is $SHA_SHORT"

Expressions and Contexts

Anywhere you see ${{ ... }}, you're inside an expression. Expressions resolve against contexts:

ContextContains
githubEvent payload, repo, ref, actor, sha, run_id
envEnvironment variables
secretsRepository, environment, or org secrets
varsNon-secret configuration variables
inputsworkflow_dispatch / workflow_call inputs
needsOutputs from prior jobs
matrixCurrent matrix variant values
stepsOutputs from prior steps (by step id)
runnerRunner OS, arch, temp paths

Conditionals

jobs:
  deploy:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - if: ${{ !cancelled() }}
        run: ./deploy.sh

Useful functions inside if: include success(), failure(), always(), cancelled(), contains(), startsWith(), endsWith(), fromJson().

Outputs Between Steps and Jobs

jobs:
  version:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.compute.outputs.tag }}
    steps:
      - id: compute
        run: echo "tag=v$(date +%s)" >> $GITHUB_OUTPUT

  release:
    needs: version
    runs-on: ubuntu-latest
    steps:
      - run: echo "Releasing ${{ needs.version.outputs.tag }}"

Concurrency

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true

This ensures only one deployment per branch runs at a time — newer commits cancel older in-flight runs. Essential for any deploy workflow.

Putting It All Together

You now have the building blocks: triggers fire workflows, workflows contain jobs, jobs contain steps, expressions resolve dynamic values, and conditionals control execution. The next four lessons go deep on the components that make these workflows actually useful: actions, runners, secrets, and reusability.

Key Takeaways

  • Triggers (on:) accept events, filters (branches/paths/tags), and manual dispatch with inputs.
  • Jobs run in parallel by default; use needs: to chain them sequentially.
  • Steps run sequentially in the same runner — they share the filesystem but not the environment by default.
  • Expressions ${{ }} evaluate using contexts (github, env, secrets, inputs, needs, matrix).
  • if: conditionals control whether a job/step runs based on expressions.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →