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
| Trigger | When it fires | Main use |
|---|---|---|
push | When you push to a branch | CI: validate new code |
pull_request | When a PR is opened/updated | Validate before merge |
schedule | On a schedule (cron) | Periodic checks (health, costs) |
workflow_dispatch | Manually from the UI | Testing, 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-ignoreto 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?
| Scenario | Push | Pull Request | Both |
|---|---|---|---|
| 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:
- PR: The code is validated before anyone approves it
- 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?
- Testing workflows: Instead of making empty commits to test changes to the workflow, you run it manually
- Manual deploys: "I want to deploy to staging NOW"
- Ad-hoc operations: "I want to run the prompt regression test now, not wait for the schedule"
- Debugging: "I want to see whether this fix resolves the workflow failure"
Recommendation: Include
workflow_dispatchin 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
| Workflow | Recommended triggers |
|---|---|
| CI (tests + lint) | push + pull_request + workflow_dispatch |
| Docker build | push to main + workflow_dispatch |
| Deploy staging | push to main + workflow_dispatch |
| Deploy production | workflow_dispatch (manual) |
| Health check | schedule + workflow_dispatch |
| Cost report | schedule (weekly) + workflow_dispatch |
| Prompt regression | pull_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"
- 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.
- Is the cron syntax correct? Use crontab.guru to validate.
- 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?
- Run tests on every PR before merge
- Check that the AI app is still responding correctly every night
- Deploy to production when the team decides to
- Build a Docker image when the code changes on main
See solution
pull_request(branches: [main]) +workflow_dispatchschedule(cron: "0 2 * * *") +workflow_dispatchworkflow_dispatchwith a confirmation inputpush(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/ortests/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:
- Every Monday and Friday at 9am UTC
- Every 4 hours
- 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 withbranches,paths,tags - ✅
pull_request— When a PR is opened/updated; filter withbranches,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)
- ✅
pathsandpaths-ignore— Avoid unnecessary CI runs when only documentation changes - ✅
github.event_name— Identifies which trigger fired the workflow for conditional behavior
Additional resources
- Events that trigger workflows - Complete list of triggers
- Crontab Guru - Visual editor for cron expressions
- Workflow trigger events - Trigger guide
- Filter pattern cheat sheet - Patterns for branches and paths
- Manual triggers with inputs - workflow_dispatch with parameters
- Scheduled events - Details on cron in Actions