A pipeline that runs at the wrong time is worse than no pipeline. In this lesson you'll master the on: block — the gatekeeper that decides when your automation fires.

The Mental Model: Events → Workflows

GitHub constantly emits events as things happen in your repository. Your workflow's on: block subscribes to specific events — like a webhook listener.

GitHub Events Stream issue.opened push (main) star.created pull_request (main) issue.commented schedule (cron) fork on: filter ci.yml — Workflow Runs! Job: lint eslint src/ Job: test npm test Job: build needs: [lint, test]
Only events matching your on: filter trigger the workflow. Everything else is ignored.
Think of on: as a subscription filter. GitHub emits hundreds of event types — your workflow only wakes up for the ones you explicitly subscribe to.

The Trigger Categories

All triggers fall into four categories. As an architect, you'll use all four:

📝 Code Events React to code changes push pull_request create (tag/branch) delete release Most common ⭐ ⏰ Scheduled Time-based triggers schedule (cron) Use for: • Nightly security scans • Weekly dependency checks • Cleanup jobs • Periodic deployments 👤 Manual Human-initiated workflow_dispatch repository_dispatch Use for: • Rollbacks • On-demand deployments • Operational tasks 🔗 Chained Triggered by other workflows workflow_call workflow_run Use for: • Reusable pipelines • Post-CI deploy • Pipeline orchestration
The four trigger categories — most workflows use Code Events + one other

Push & Pull Request: The Workhorses

90% of your triggers will be push and pull_request. Here's the crucial difference:

on: push 💻 git push 🚀 Runs! Fires when commits land on the branch on: pull_request 💻 Open PR 🚀 Runs! Fires when PR is opened, updated, or reopened ⚡ Architect Insight: Use BOTH Together pull_request → validates the change BEFORE it reaches main (gatekeeper) push to main → triggers the deployment AFTER the change is merged

Branch & Path Filtering

Raw triggers are too broad. Filters narrow them to exactly what you need:

Branch Filters

on:
  push:
    branches:
      - main            # exact match
      - 'release/**'    # glob: release/1.0, release/2.0, etc.
    branches-ignore:
      - 'dependabot/**' # skip auto-update branches

Path Filters (Critical for Mono-Repos)

on:
  push:
    paths:
      - 'src/**'             # Only when source code changes
      - 'package-lock.json'  # Or when deps change
    paths-ignore:
      - '**.md'              # Never run for docs changes
      - 'docs/**'
Files changed in push: ✓ src/calculator.js ✓ src/server.js ✗ README.md ✗ docs/setup.md ✅ Pipeline RUNS At least one file matches paths: ['src/**']
Path filters prevent wasted pipeline runs — if only docs changed, why rebuild?
Architect decision: Path filters save minutes and money. In a mono-repo with 10 services, you don't want ALL services rebuilding when only one service's code changed. This is how you solve it.

Tag Triggers (Releases)

on:
  push:
    tags:
      - 'v*'           # v1.0.0, v2.3.1, v0.1.0-rc.1
      - '!v*-rc*'      # exclude release candidates

This is how release pipelines work: you tag a commit → pipeline builds and publishes the release. We'll build this in a later lesson.

Schedule (Cron Syntax)

on:
  schedule:
    - cron: '0 2 * * 1'   # Every Monday at 2:00 AM UTC
0 2 * * 1 minute (0-59) hour (0-23) day (1-31) month (1-12) weekday (0=Sun) Common crons: '0 * * * *' = hourly '0 0 * * *' = midnight daily '0 6 * * 1' = Mon 6AM
Cron syntax: minute hour day-of-month month day-of-week (* = any)

Manual Trigger: workflow_dispatch

This adds a "Run workflow" button in the GitHub UI. Essential for operational tasks:

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options:
          - staging
          - production
      reason:
        description: 'Why are you deploying?'
        required: true
        type: string
Run workflow Branch: main ▾ Target environment: staging ▾ Why are you deploying? Hotfix for login bug Run workflow
workflow_dispatch creates an interactive form in the GitHub Actions UI

Access inputs in your workflow with ${{ inputs.environment }}

🏋️ Exercise: Multi-Trigger Workflow

Update your ci.yml to use multiple triggers with filters:

name: CI

on:
  push:
    branches: [main]
    paths-ignore:
      - '**.md'
  pull_request:
    branches: [main]
  workflow_dispatch:  # manual trigger too

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test
      - name: Show trigger info
        run: |
          echo "Triggered by: ${{ github.event_name }}"
          echo "Branch: ${{ github.ref_name }}"

Test it three ways:

  1. Push to main → watch it trigger
  2. Edit only README.md and push → it should NOT trigger (paths-ignore)
  3. Go to Actions tab → click "Run workflow" → it runs manually

Decision Framework: Which Trigger When?

ScenarioTriggerWhy
Validate PR before mergepull_requestGatekeeper — catch bugs before main
Deploy after mergepush: branches: [main]Only deploy validated code
Create a releasepush: tags: ['v*']Version tag = release intent
Nightly security scanschedule: cronCatch new CVEs even without code changes
Emergency rollbackworkflow_dispatchHuman-initiated, parameterised
Mono-repo: only changed servicepush: paths: ['svc-a/**']Don't waste time rebuilding unchanged services

🧠 Recall Check

  1. What's the difference between on: push and on: pull_request in terms of when they fire?
  2. You change only README.md. Your workflow has paths-ignore: ['**.md']. Does it run?
  3. How do you make a workflow runnable manually from the GitHub UI?
  4. What trigger would you use for a "scan for vulnerabilities every night" workflow?
Reveal answers
  1. push fires when commits land on a branch (after merge/direct push). pull_request fires when a PR is opened, updated, or reopened (before merge).
  2. No. The only changed file matches paths-ignore, so the workflow is skipped.
  3. Add workflow_dispatch: to the on: block. Optionally define inputs: for parameters.
  4. schedule with a cron expression, e.g., cron: '0 2 * * *' for 2 AM daily.
You now control when your pipeline runs — and equally important, when it doesn't. Next lesson: controlling how work is organised within a pipeline (jobs, parallelism, dependencies).