Module 1: Introduction to CI/CD and GitHub Actions

5. Triggers — Push, PR, Schedule, Workflow Dispatch

Overview

A workflow without a trigger is like an alarm without a sensor — it exists but never goes off. Triggers define when your workflow runs: on every push, when someone opens a PR, at 3am every day, or when you decide manually. Choosing the right trigger is as important as defining the workflow's steps.

In this capsule you're going to learn the 4 triggers you'll use in 95% of your workflows, when to use each one, and how to combine them. By the end, you'll know exactly which trigger each type of workflow in your AI pipeline needs.


The 4 essential triggers

TriggerWhen it firesMain use
pushWhen you push to a branchCI: validate new code
pull_requestWhen a PR is opened/updatedValidate before merge
scheduleOn a schedule (cron)Periodic checks (health, costs)
workflow_dispatchManually from the UITesting, manual deploys

Trigger: push

Fires every time you push commits to a branch.

Basic form

on: push     # Fires on ANY push to ANY branch

Filter by branches

on:
  push:
    branches:
      - main              # Only pushes to main
      - develop           # And pushes to develop

Filter by branches with patterns

on:
  push:
    branches:
      - main
      - "release/*"       # release/1.0, release/2.0, etc.
      - "feature/**"      # feature/login, feature/auth/oauth, etc.

Difference between * and **:

  • * matches one level: release/*release/1.0 (yes), release/1.0/hotfix (no)
  • ** matches multiple levels: feature/**feature/auth/oauth (yes)

Filter by paths (only certain files)

on:
  push:
    branches: [main]
    paths:
      - "src/**"              # Only if files changed in src/
      - "tests/**"            # Or in tests/
      - "requirements.txt"    # Or requirements.txt

What's it for? If you only change the README.md, you don't need to run the whole test suite. Filtering by paths saves unnecessary CI minutes.

Ignore paths

on:
  push:
    branches: [main]
    paths-ignore:
      - "*.md"                # Ignore changes in markdown files
      - "docs/**"             # Ignore changes in documentation
      - ".gitignore"

Tip for AI projects: Your documentation files, prompts in markdown, and READMEs don't need to fire the testing pipeline. Use paths-ignore to avoid unnecessary CI runs.

Filter by tags

on:
  push:
    tags:
      - "v*"                  # Tags that start with v: v1.0.0, v2.1.3

This is useful for release workflows: when you create a v1.0.0 tag, a workflow fires that builds and deploys that version.


Trigger: pull_request

Fires when someone opens, updates, or reopens a Pull Request.

Basic form

on: pull_request    # Any PR toward any branch

Filter by target branch

on:
  pull_request:
    branches:
      - main              # Only PRs going toward main

Activity types

By default, pull_request fires on opened, synchronize (new push to the PR), and reopened. You can be more specific:

on:
  pull_request:
    types:
      - opened             # PR just created
      - synchronize        # New push to the PR's branch
      - reopened           # PR that was reopened
      - ready_for_review   # Draft PR marked as ready

Filter by paths (same as push)

on:
  pull_request:
    branches: [main]
    paths:
      - "src/**"
      - "tests/**"

Push vs Pull Request: When to use each?

ScenarioPushPull RequestBoth
CI on every commit
Validate before merge
Complete pipeline
Automatic deploy to staging✅ (to main)
Checks on PRs (status checks)

The most common pattern is to use both:

on:
  push:
    branches: [main]           # CI on main after the merge
  pull_request:
    branches: [main]           # CI on the PR before the merge

This gives you two layers of protection:

  1. PR: The code is validated before anyone approves it
  2. Push to main: Final validation after the merge (in case the merge itself introduced a conflict)

Trigger: schedule

Runs the workflow on a schedule defined with cron syntax.

Cron syntax

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23, UTC)
│ │ ┌───────────── day of the month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of the week (0-6, 0=Sunday)
│ │ │ │ │
* * * * *

Common examples

on:
  schedule:
    # Every day at 3am UTC
    - cron: "0 3 * * *"

    # Every Monday at 8am UTC
    - cron: "0 8 * * 1"

    # Every hour
    - cron: "0 * * * *"

    # The first day of every month at midnight
    - cron: "0 0 1 * *"

Scheduled workflows for AI systems

Scheduled workflows are particularly valuable for AI because LLM providers update their models without telling you:

on:
  schedule:
    # Every night at 2am UTC: check that the prompts still work
    - cron: "0 2 * * *"

    # Every Monday: cost report for the week
    - cron: "0 8 * * 1"

    # Every 6 hours: API health check
    - cron: "0 */6 * * *"

Why does it matter?

Without scheduled workflows:

Monday: OpenAI updates gpt-4o-mini
Tuesday: The quality of your responses changes
Wednesday: A user reports that "something's off"
Thursday: You investigate, discover the model change
Friday: You apply a fix

With scheduled workflows:

Monday: OpenAI updates gpt-4o-mini
Tuesday 2am: Scheduled workflow detects a change in the baseline
Tuesday 7am: Slack notification: "Prompt regression detected"
Tuesday 9am: You investigate and apply a fix

Important limitations

  • ✅ Cron uses UTC, not your local time zone
  • ⚠️ GitHub can delay scheduled workflows by up to 15-60 minutes during periods of high demand
  • ⚠️ If the repo hasn't had activity in 60 days, scheduled workflows are disabled automatically
  • ⚠️ The minimum practical interval is every 5 minutes, but GitHub recommends no less than every 15 minutes

Trigger: workflow_dispatch

Lets you run the workflow manually from GitHub's UI or via the API.

Basic form

on:
  workflow_dispatch:    # No inputs, just a "Run workflow" button

This adds a "Run workflow" button in your repo's Actions tab.

With inputs (parameters)

on:
  workflow_dispatch:
    inputs:
      environment:
        description: "Environment to deploy to"
        required: true
        type: choice
        options:
          - staging
          - production
      dry_run:
        description: "Dry run (no deploy)"
        required: false
        type: boolean
        default: true

You access the inputs in the workflow:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: |
          echo "Deploying to ${{ inputs.environment }}"
          if [ "${{ inputs.dry_run }}" == "true" ]; then
            echo "DRY RUN - no actual deploy"
          fi

Why is workflow_dispatch invaluable?

  1. Testing workflows: Instead of making empty commits to test changes to the workflow, you run it manually
  2. Manual deploys: "I want to deploy to staging NOW"
  3. Ad-hoc operations: "I want to run the prompt regression test now, not wait for the schedule"
  4. Debugging: "I want to see whether this fix resolves the workflow failure"

Recommendation: Include workflow_dispatch in ALL your workflows from day one. It costs nothing and saves you hours of debugging.


Combining triggers

Triggers can be combined. The workflow runs when any of the triggers fires (OR, not AND):

The most used pattern: Push + PR + Dispatch

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

Complete CI with schedule

on:
  push:
    branches: [main]
    paths:
      - "src/**"
      - "tests/**"
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 2 * * *"           # Nightly: detect model updates
  workflow_dispatch:

Which trigger to use for each workflow

WorkflowRecommended triggers
CI (tests + lint)push + pull_request + workflow_dispatch
Docker buildpush to main + workflow_dispatch
Deploy stagingpush to main + workflow_dispatch
Deploy productionworkflow_dispatch (manual)
Health checkschedule + workflow_dispatch
Cost reportschedule (weekly) + workflow_dispatch
Prompt regressionpull_request + schedule + workflow_dispatch

How to know which trigger fired the workflow

Inside the workflow you can find out which trigger fired it:

jobs:
  info:
    runs-on: ubuntu-latest
    steps:
      - name: Show trigger
        run: |
          echo "Event: ${{ github.event_name }}"
          echo "Ref: ${{ github.ref }}"
          echo "SHA: ${{ github.sha }}"

This is useful when a workflow has multiple triggers and you need different behavior:

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

  deploy:
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying (only on push to main, not on PRs)"

Troubleshooting

"My scheduled workflow doesn't run"

  1. Has the repo had recent activity? If there are no pushes or PRs in 60 days, GitHub disables scheduled workflows. Make a push to reactivate.
  2. Is the cron syntax correct? Use crontab.guru to validate.
  3. Is it on the default branch (main)? Scheduled workflows only run from the repo's default branch.

"My workflow runs twice"

If you have on: [push, pull_request] and you push to a branch that has an open PR, the workflow runs twice: once for the push and once for the PR. This is normal and expected — each run validates from a different perspective.

To avoid it in small repos:

on:
  push:
    branches: [main]              # Push only on main (after the merge)
  pull_request:
    branches: [main]              # PR toward main (before the merge)

"workflow_dispatch doesn't show up"

The "Run workflow" button only appears if the YAML with workflow_dispatch is already on the default branch (main). If you added the trigger on a feature branch, you need to merge it to main first.


Exercises

Exercise 1: Choose the trigger

For each scenario, which trigger(s) would you use?

  1. Run tests on every PR before merge
  2. Check that the AI app is still responding correctly every night
  3. Deploy to production when the team decides to
  4. Build a Docker image when the code changes on main
See solution
  1. pull_request (branches: [main]) + workflow_dispatch
  2. schedule (cron: "0 2 * * *") + workflow_dispatch
  3. workflow_dispatch with a confirmation input
  4. push (branches: [main], paths: ["src/**", "Dockerfile"]) + workflow_dispatch

Pattern: Always include workflow_dispatch as a backup for running manually.

Exercise 2: Write the trigger

Write the on: block for a workflow that:

  • Runs on push to main, but ONLY if files in src/ or tests/ change
  • Runs on PRs toward main
  • Runs every day at 6am UTC
  • Can be run manually
See solution
on:
  push:
    branches: [main]
    paths:
      - "src/**"
      - "tests/**"
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 6 * * *"
  workflow_dispatch:

Exercise 3: Cron syntax

Write the cron expression for:

  1. Every Monday and Friday at 9am UTC
  2. Every 4 hours
  3. The 15th of every month at midnight
See solution
# 1. Monday and Friday at 9am UTC
- cron: "0 9 * * 1,5"

# 2. Every 4 hours
- cron: "0 */4 * * *"

# 3. The 15th of every month at midnight
- cron: "0 0 15 * *"

Check on crontab.guru if you have doubts.

Exercise 4: Avoid the double run

Your workflow runs twice on every push to a branch with an open PR. Rewrite the trigger so this doesn't happen:

# Problem: double execution
on:
  push:
  pull_request:
See solution
on:
  push:
    branches: [main]              # Push ONLY on main (post-merge)
  pull_request:
    branches: [main]              # PR toward main (pre-merge)
  workflow_dispatch:

Now: pushes to feature branches don't fire the push trigger (only the PR trigger fires when there's an open PR). The push trigger only runs when the merge to main happens.


Summary

  • push — Every push to a branch; filter with branches, paths, tags
  • pull_request — When a PR is opened/updated; filter with branches, paths, types
  • schedule — Cron syntax in UTC; useful for nightly health checks and prompt regression
  • workflow_dispatch — Manual trigger; include it in ALL your workflows
  • Combine triggers — A workflow can have multiple triggers (OR logic)
  • paths and paths-ignore — Avoid unnecessary CI runs when only documentation changes
  • github.event_name — Identifies which trigger fired the workflow for conditional behavior

Additional resources

  1. Events that trigger workflows - Complete list of triggers
  2. Crontab Guru - Visual editor for cron expressions
  3. Workflow trigger events - Trigger guide
  4. Filter pattern cheat sheet - Patterns for branches and paths
  5. Manual triggers with inputs - workflow_dispatch with parameters
  6. Scheduled events - Details on cron in Actions