Switch Language
Toggle Theme

GitHub Actions Getting Started: YAML Workflow Basics and Trigger Configuration

It’s 3 AM, and that red error message on your screen makes you want to smash your keyboard.

Your code runs perfectly locally, but the moment you push to GitHub, it fails. I edited that YAML file six times—every single time, it was an indentation issue. I remember thinking: why is this harder than writing actual code?

The truth is, GitHub Actions itself isn’t complicated. What’s difficult is the documentation—you’re immediately hit with hundreds of pages of configuration details that leave you dizzy. In this article, I want to walk you through the core structure of YAML workflows in the simplest way possible.

You’ll learn:

  • The four core fields of YAML files and what each one does
  • 8 common trigger configuration methods and their use cases
  • A complete workflow template you can copy and use immediately
  • The pitfalls I’ve encountered and how to avoid them

Ready? Let’s dive in.

YAML Workflow Files: Four Core Fields

Honestly, when I first started with GitHub Actions, seeing those YAML files in the .github/workflows directory felt like deciphering alien script. Indentation everywhere, colons everywhere—miss one space and everything breaks.

Later I discovered it really boils down to four core parts. Master these four, and everything else is just icing on the cake.

name: Give Your Workflow a Name

This field is the simplest, yet many people (myself included) overlook it at first.

name: CI for Node.js App

name is what your workflow displays in the GitHub Actions tab. After you push code, when you go to the Actions page in your repository, that’s the text you see.

Here’s a naming tip: use project name + function description. Like MyApp CI or Backend Deploy. This way, when you have multiple workflows later, you can immediately spot the one you’re looking for.

This field is technically optional. If you omit it, GitHub will use the filename instead. But I don’t recommend skipping it—filenames are usually abbreviated, which isn’t as intuitive.

on: When to Trigger

8 types
Common Triggers

on is the “switch” for your entire workflow. You’re telling GitHub: under what circumstances should this workflow run.

The simplest form:

on: push

This means: trigger whenever there’s any code push.

But in real projects, you typically need more granular control. Like only running when pushing to the main branch:

on:
  push:
    branches: [main]

Or you might want to trigger when creating a Pull Request too:

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

Triggers are the essence of GitHub Actions. I’ll dedicate a whole section later to cover 8 common scenarios. For now, just remember: on determines the “trigger timing” of your workflow.

jobs: Define What to Do

jobs is the body of your workflow, defining “what specific tasks to execute.”

A workflow can contain multiple jobs, which run in parallel by default. Each job needs to specify its runtime environment using the runs-on field:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # ... steps list

This code defines a job called build that will run on the latest Ubuntu version provided by GitHub.

If your workflow has multiple jobs, you can use the needs field to define dependencies:

jobs:
  test:
    runs-on: ubuntu-latest
    # ... test steps

  deploy:
    needs: test  # Wait for test to complete before executing
    runs-on: ubuntu-latest
    # ... deploy steps

This way, deploy will wait for test to finish before starting. If test fails, deploy won’t run.

steps: Specific Execution Steps

steps are the smallest execution units within a job—commands or actions executed in sequence.

Each step has two ways to write it:

1. Use run to execute commands:

steps:
  - name: Install dependencies
    run: npm ci

  - name: Run tests
    run: npm test

After run is the command you want to execute in the terminal, just like typing commands locally.

2. Use uses to call Actions:

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Setup Node.js
    uses: actions/setup-node@v4
    with:
      node-version: '20'

uses is GitHub Actions’ killer feature. Someone else has already written an Action, and you just use it directly. For example, actions/checkout@v4 pulls your code, and actions/setup-node@v4 sets up your Node.js environment.

with passes parameters to the Action. Like node-version: '20' above tells setup-node: I want to use Node.js version 20.

That covers all four fields. Simpler than you imagined, right?

Trigger Deep Dive: 8 Common Scenarios

Earlier I mentioned the on field determines trigger timing. GitHub Actions supports dozens of triggers, but honestly, only a handful are commonly used.

I’ve put together a table to help you quickly understand each trigger’s use case:

TriggerTypical ScenarioConfiguration Example
pushCode pushed to branchon: push: branches: [main]
pull_requestPR created or updatedon: pull_request: types: [opened, synchronize]
scheduleScheduled tasks (Cron)on: schedule: - cron: '0 0 * * *'
workflow_dispatchManual triggeron: workflow_dispatch: inputs: env: ...
workflow_callReusable workflowon: workflow_call: inputs: ...
releaseRelease eventson: release: types: [published]
issuesIssue eventson: issues: types: [opened, labeled]
repository_dispatchExternal eventson: repository_dispatch: types: [deploy]

Let me elaborate on a few of the most commonly used ones.

push: The Most Basic Trigger

push is the first trigger you’ll encounter. It fires when code is pushed to a branch.

Simple configuration:

on: push

But there’s a problem with this configuration: pushes to any branch will trigger it. If your repository has 20 branches, every push from anyone will run the workflow, and you’ll burn through your free quota quickly.

A more sensible approach is to limit branches:

on:
  push:
    branches: [main, develop]

Or use wildcards:

on:
  push:
    branches:
      - 'main'
      - 'release/**'  # Matches release/v1.0, release/v2.0, etc.

pull_request: Guardian Before Code Merge

pull_request triggers when a PR is created or updated, typically used for running tests and checking code style.

on:
  pull_request:
    branches: [main]

You can use the types field for more granular control:

on:
  pull_request:
    types: [opened, synchronize, reopened]
  • opened: PR just created
  • synchronize: PR has new commits
  • reopened: PR reopened

With this configuration, the workflow only runs under these three circumstances, avoiding wasted resources.

schedule: Scheduled Tasks

schedule uses Cron expressions to define scheduled triggers. Like running tests once every day at midnight:

on:
  schedule:
    - cron: '0 0 * * *'  # Every day at UTC 0:00

Cron expressions have 5 fields: minute, hour, day, month, day of week.

Some common timings:

  • 0 0 * * *: Every day at UTC 0:00 (8 AM Beijing time)
  • 0 */6 * * *: Every 6 hours
  • 30 2 * * 1: Every Monday at UTC 2:30

One pitfall to watch out for: GitHub uses UTC time. If you want to run a task at 9 AM Beijing time, you need to set it to UTC 1:00 (0 1 * * *).

workflow_dispatch: Manual Trigger

Sometimes you don’t want automatic triggering; you want to click a button to run it manually. That’s what workflow_dispatch is for.

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

After configuration, a “Run workflow” button appears on the Actions page. Click it to select environment parameters before triggering.

This trigger is particularly useful in deployment scenarios: automated testing, manual deployment.

workflow_call: Reusable Workflows

If your workflow logic is complex, or multiple repositories need the same process, you can use workflow_call to turn your workflow into a reusable component.

Define a reusable workflow:

# .github/workflows/ci.yml
on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string

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 && npm test

Call it from another workflow:

# .github/workflows/main.yml
on: push

jobs:
  call-ci:
    uses: ./.github/workflows/ci.yml
    with:
      node-version: '20'

This lets you reuse the same CI logic across different repositories—change it once, it takes effect everywhere.

The other triggers (release, issues, repository_dispatch) are used less frequently, so I won’t expand on them. If you’re interested, check out the official GitHub documentation.

Hands-on: Your First Workflow Template

Enough concepts—let’s get our hands dirty.

Here’s a complete Node.js project CI workflow. You can copy this directly into your own project:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # 1. Checkout code
      - name: Checkout code
        uses: actions/checkout@v4

      # 2. Setup Node.js environment
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      # 3. Install dependencies
      - name: Install dependencies
        run: npm ci

      # 4. Run tests
      - name: Run tests
        run: npm test

How to Use It?

Step 1: Create a .github/workflows folder in your project root (if it doesn’t already exist).

Step 2: Create a ci.yml file in that folder and paste the code above.

Step 3: Commit and push to GitHub.

After pushing, go to your repository’s Actions tab, and you should see your workflow running.

Line-by-Line Explanation

  • name: CI: Workflow name, displays in the Actions page
  • on: push: branches: [main]: Triggers when code is pushed to the main branch
  • on: pull_request: branches: [main]: Also triggers when someone opens a PR to main
  • jobs: build:: Defines a job called build
  • runs-on: ubuntu-latest: Runs on the latest Ubuntu provided by GitHub
  • actions/checkout@v4: Official Action that pulls your code to the virtual machine
  • actions/setup-node@v4: Official Action that sets up the Node.js environment
  • npm ci: Installs dependencies (faster and cleaner than npm install)
  • npm test: Runs tests

If your project isn’t Node.js, just swap out the middle steps. For a Python project:

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-python@v5
    with:
      python-version: '3.11'
  - run: pip install -r requirements.txt
  - run: pytest

It’s basically a “pull code → setup environment → install dependencies → run tests” pattern.

Common Configuration Errors Troubleshooting

I’ve stepped in these pits so you don’t have to.

Here’s a troubleshooting checklist to reference when you encounter issues:

Error SymptomPossible CauseSolution
Workflow doesn’t triggerBranch filter conditions wrongCheck branches configuration, confirm branch name case is correct
YAML parsing failsUsing Tab for indentationChange all to space indentation, YAML doesn’t support Tab
Secrets don’t workScope errorVerify secrets are at the correct level (job or step)
Job dependency failsneeds reference errorCheck job-id spelling, case-sensitive
Workflow runs slowlyNot using cacheAdd actions/cache to cache dependencies
Permission denied errorsGITHUB_TOKEN insufficient permissionsAdd permissions configuration in job

Error 1: Wrong Indentation

This is the most common error, bar none.

YAML is extremely sensitive to indentation. You must use spaces, not Tab. And each level of indentation must be exactly 2 spaces (or some other consistent number, but GitHub Actions defaults to 2).

# Wrong example: indentation messed up
jobs:
  build:
  runs-on: ubuntu-latest  # This line should be indented 4 spaces
# Correct way
jobs:
  build:
    runs-on: ubuntu-latest  # Indented 4 spaces

Most editors (VS Code, WebStorm) can be configured to “automatically insert spaces when pressing Tab”. I recommend turning this on so you don’t have to manually type spaces every time.

Error 2: Wrong Branch Name

GitHub branch names are case-sensitive. main and Main are two different branches.

# If your branch is called main
on:
  push:
    branches: [Main]  # Wrong, won't trigger

# Correct way
on:
  push:
    branches: [main]  # Lowercase

If you’re not sure about the branch name, check the GitHub repository page or run git branch locally.

Error 3: job-id Spelling Error

When your workflow has multiple jobs with dependencies, the needs field must accurately reference other job ids.

jobs:
  test:
    runs-on: ubuntu-latest
    # ...

  deploy:
    needs: Test  # Wrong, case doesn't match
    runs-on: ubuntu-latest
# Correct way
jobs:
  test:
    runs-on: ubuntu-latest

  deploy:
    needs: test  # Lowercase, matching the job id above
    runs-on: ubuntu-latest

Error 4: Secrets Used at Wrong Level

GitHub Secrets have two scopes: repository level and environment level. Reference them with ${{ secrets.XXX }}.

# Wrong example: secrets written in wrong position
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      API_KEY: secrets.MY_KEY  # Wrong, missing ${{ }}
# Correct way
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      API_KEY: ${{ secrets.MY_KEY }}  # Wrapped with ${{ }}

Also, Secrets are encrypted—you can’t see their actual values in logs. To confirm whether a value was passed successfully, you can print a substitute:

- name: Debug
  run: echo "API_KEY is set: ${{ secrets.MY_KEY != '' }}"

This won’t leak the actual Key, but lets you confirm whether it’s empty or not.

FAQ

What fields must a GitHub Actions workflow contain?
The minimal configuration only needs `on` (trigger) and `jobs` (task definition). `name` and `steps` are optional but not recommended to omit—they make your workflow more readable.
What's the difference between push and pull_request triggers?
`push` triggers when code is pushed to a branch, suitable for deployment processes; `pull_request` triggers when a PR is created or updated, suitable for code checks and testing. They're typically used together: run tests on PR, run deployment after merge.
Should I use Tab or spaces for YAML indentation?
You must use spaces. YAML doesn't support Tab indentation. I recommend enabling the 'insert spaces when pressing Tab' setting in VS Code to avoid this pitfall. Each indentation level typically uses 2 spaces.
What's the free quota? What happens if I exceed it?
Private repositories get 2000 minutes per month, public repositories have no limit. If you exceed it, you can purchase additional quota or use self-hosted runners (self-hosted environments don't count toward the free quota).

GitHub Actions vs Other CI/CD Tools

You might have used Jenkins, GitLab CI, or CircleCI. How does GitHub Actions compare?

I’ve put together a comparison table:

ComparisonGitHub ActionsGitLab CIJenkinsCircleCI
Configuration LanguageYAMLYAMLGroovyYAML
HostingCloud-nativeCloud/Self-hostedSelf-hostedCloud-native
IntegrationGitHub nativeGitLab nativeRequires setupRequires setup
Learning CurveLowLowHighMedium
Free Quota2000 min/month (private repos)400 min/monthUnlimited (self-hosted)6000 min/month
Public RepositoriesUnlimitedUnlimitedUnlimitedUnlimited

GitHub Actions Advantages

1. Zero-Configuration Integration

If your code is already hosted on GitHub, using GitHub Actions is the natural choice. No need to configure webhooks, no need to maintain servers, no need to install plugins. Create a YAML file, push it, and it runs.

2. Marketplace Ecosystem

GitHub Marketplace has thousands of Actions. AWS, Azure, Google Cloud all have official Actions. Whatever functionality you need, chances are someone has already written it—just uses it.

3. Developer-Friendly Free Quota

Unlimited use for public repositories, 2000 minutes per month for private repositories. For personal projects or small teams, it’s basically sufficient.

When Not to Choose GitHub Actions?

1. Your Code Isn’t on GitHub

If you use GitLab or Bitbucket, use their built-in CI/CD. While GitHub Actions can be triggered via webhooks, going through that hassle isn’t as good as using the native solution.

2. You Need Complete Control Over the Runtime Environment

GitHub Actions runner environments are fixed (Ubuntu/Windows/macOS). You can’t install your own software or configure special environments. In this case, Jenkins + self-hosted runner is more flexible.

3. You Have Extreme Security Requirements

GitHub Actions runners are GitHub-hosted virtual machines. If your code involves highly sensitive information, you might need to build your own CI system. Though GitHub also supports self-hosted runners, which can be a middle-ground solution.

My Recommendation

For most individual developers and small teams:

  • Code on GitHub → Choose GitHub Actions
  • Code on GitLab → Choose GitLab CI
  • Need high customization → Jenkins or self-hosted runner

There’s no absolute best solution—what fits you is what’s best.

Summary

By now, you should have a solid understanding of how GitHub Actions YAML workflows work.

Key takeaways:

  • Four core fields: name for naming, on for triggers, jobs for defining tasks, steps for execution steps
  • Eight common triggers: most used are push, pull_request, and schedule
  • One copy-paste template: pull code → setup environment → install dependencies → run tests
  • Four common pitfalls: indentation, branch names, job-id, and Secrets

Next steps:

  1. Create your first workflow in your project (just copy the template from this article and adapt it to your project configuration)
  2. Read the follow-up article in this series, “GitHub Actions Caching Strategies: Speed Up CI/CD Pipelines by 5x”, to learn how to make workflows run faster
  3. Browse GitHub Marketplace to see what useful Actions you can use directly

Once you taste the sweetness of automation, there’s no going back.

11 min read · Published on: Apr 10, 2026 · Modified on: Apr 11, 2026

Comments

Sign in with GitHub to leave a comment

Related Posts