GitHub Actions YAML Guide: Complete Workflow Reference
Everything you need to write, debug, and optimize GitHub Actions CI/CD workflows.
Introduction
GitHub Actions is one of the most widely-adopted CI/CD platforms, integrated directly into GitHub repositories. Workflows are defined in YAML files stored in the .github/workflows/ directory. This guide provides a complete reference for writing GitHub Actions workflows, from basic triggers to advanced patterns like matrix strategies and reusable workflows.
If you are new to YAML itself, start with our YAML vs JSON vs TOML comparison for a solid foundation. If you already know YAML and want to understand how GitHub Actions interprets it, read on.
Workflow File Structure
Every GitHub Actions workflow file is a YAML document with three top-level keys: name (optional but recommended), on (the trigger), and jobs (the work to perform). Here is a minimal but complete workflow:
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: npm testThis workflow runs on every push to main and on every pull request targeting main. It checks out the code, installs Node.js 20, installs dependencies, and runs tests. Let us break down each section.
Workflow Triggers (on)
The on key defines when the workflow runs. GitHub Actions supports dozens of trigger events. Here are the most commonly used ones.
Push and Pull Request Events
# Run on push to specific branches
on:
push:
branches:
- main
- "release/**" # glob patterns work
paths:
- "src/**" # only when source files change
- "package.json"
paths-ignore:
- "docs/**" # skip documentation-only changes
# Run on pull request events
on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]Schedule (Cron)
Scheduled workflows use cron syntax. The schedule uses UTC time. For help building cron expressions, try our Cron Expression Builder.
# Run at 9:00 AM UTC every weekday
on:
schedule:
- cron: "0 9 * * 1-5"
# Run at midnight on the first day of every month
on:
schedule:
- cron: "0 0 1 * *"Manual Triggers
# workflow_dispatch allows manual triggering from the GitHub UI
on:
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
default: "staging"
type: choice
options:
- staging
- production
dry_run:
description: "Perform a dry run"
required: false
type: boolean
default: falseOther Useful Triggers
# Run when a release is published
on:
release:
types: [published]
# Run when an issue is opened or labeled
on:
issues:
types: [opened, labeled]
# Run when called by another workflow
on:
workflow_call:
inputs:
node-version:
required: true
type: stringJobs
Jobs define the units of work in a workflow. By default, jobs run in parallel. You can create dependencies between jobs using the needs keyword.
Job Dependencies
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
# Deploy only after lint AND test both succeed
deploy:
needs: [lint, test]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: npm run deployJob Outputs
Jobs can pass data to downstream jobs using outputs.
jobs:
version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
- id: get_version
run: echo "version=$(cat package.json | jq -r .version)" >> "$GITHUB_OUTPUT"
publish:
needs: version
runs-on: ubuntu-latest
steps:
- run: echo "Publishing version ${{ needs.version.outputs.version }}"Steps
Steps are the individual tasks within a job. Each step either runs a shell command (run) or uses a reusable action (uses).
Multi-line Run Commands
steps:
- name: Build and test
run: |
npm ci
npm run build
npm test
working-directory: ./app
- name: Set up environment
run: |
echo "NODE_ENV=production" >> $GITHUB_ENV
echo "$(pwd)/bin" >> $GITHUB_PATHConditional Steps
steps:
- name: Run on main only
if: github.ref == 'refs/heads/main'
run: npm run deploy
- name: Run on failure
if: failure()
run: echo "Something went wrong"
- name: Run always (even if previous steps failed)
if: always()
run: npm run cleanupMatrix Strategies
Matrix strategies let you run the same job with different configurations. This is essential for testing across multiple operating systems, language versions, or dependency versions.
jobs:
test:
strategy:
fail-fast: false # Don't cancel other matrix jobs if one fails
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18 # Skip Node 18 on Windows
include:
- os: ubuntu-latest
node: 22
experimental: true
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental || false }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm testThis creates a job for each combination of OS and Node version (9 total), minus the excluded combination (Windows + Node 18), plus the special inclusion. The fail-fast: false option ensures that if one combination fails, the others continue running so you get the full picture.
Secrets and Environment Variables
Using Secrets
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to production
env:
API_KEY: ${{ secrets.DEPLOY_API_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ./dist s3://my-bucket
curl -X POST https://api.example.com/deploy \
-H "Authorization: Bearer $API_KEY"Environment Variables
# Workflow-level env vars
env:
NODE_ENV: production
CI: true
jobs:
build:
runs-on: ubuntu-latest
# Job-level env vars (override workflow-level)
env:
DATABASE_URL: postgres://localhost:5432/testdb
steps:
- name: Build
# Step-level env vars (override job-level)
env:
NEXT_PUBLIC_API_URL: https://api.example.com
run: npm run buildReusable Workflows
Reusable workflows allow you to define a workflow once and call it from other workflows. This is the GitHub Actions equivalent of a function or template.
Defining a Reusable Workflow
# .github/workflows/reusable-test.yml
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
required: true
type: string
secrets:
npm-token:
required: false
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
env:
NPM_TOKEN: ${{ secrets.npm-token }}
- run: npm testCalling a Reusable Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
jobs:
test-node-20:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20"
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
test-node-22:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "22"
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}Composite Actions
Composite actions bundle multiple steps into a single reusable action. They are defined in an action.yml file and can be shared across workflows and repositories.
# .github/actions/setup-and-build/action.yml
name: "Setup and Build"
description: "Install dependencies and build the project"
inputs:
node-version:
description: "Node.js version"
required: false
default: "20"
runs:
using: "composite"
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: "npm"
- run: npm ci
shell: bash
- run: npm run build
shell: bashCommon Patterns and Anti-Patterns
Pattern: Caching Dependencies
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm" # Built-in caching for npm
- run: npm ci # Uses cache automaticallyPattern: Concurrency Control
# Cancel in-progress runs when a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueAnti-Pattern: Hardcoded Versions
# Bad: hardcoded action versions
- uses: actions/checkout@v3 # outdated
# Good: pin to major version for automatic minor/patch updates
- uses: actions/checkout@v4
# Best: pin to a specific SHA for maximum reproducibility
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11Anti-Pattern: Secrets in Logs
# Bad: this prints the secret to the log
- run: echo "The token is ${{ secrets.API_TOKEN }}"
# Good: use secrets in env vars, never echo them
- run: curl -H "Authorization: Bearer $TOKEN" https://api.example.com
env:
TOKEN: ${{ secrets.API_TOKEN }}YAML-Specific Tips for GitHub Actions
GitHub Actions has some YAML-specific behaviors that are worth knowing:
- No YAML anchors: Unlike GitLab CI, GitHub Actions does not support YAML anchors and aliases. The workflow parser strips them. Use reusable workflows or composite actions instead.
- Expression syntax: The
${{ }}syntax is GitHub's expression language, not YAML. It is evaluated by the Actions runner before the YAML is fully parsed. Always quote expressions that start with*or!to avoid YAML parsing issues. - Multi-line strings: Use YAML's literal block scalar (
|) for multi-lineruncommands. Each line is executed as a separate shell command, and the step fails if any line returns a non-zero exit code. - Boolean values: YAML's implicit boolean conversion can cause issues. The value
on(as in the trigger key) is technically a YAML boolean. GitHub Actions handles this correctly, but if you useonoroffas string values elsewhere, always quote them.
Validate your workflow YAML before pushing with our YAML Validator. The K8s/Actions tab can automatically detect GitHub Actions workflow files and highlight potential issues.
Further Reading
- GitHub Actions workflow syntax
Official GitHub documentation for workflow YAML file syntax.
- YAML 1.2 specification
The complete YAML language specification.
- GitHub expression syntax
Reference for expressions used in GitHub Actions workflow files.