Skip to content

Triggers & Events

A workflow does nothing until something tells it to run. That "something" is its trigger — the on: key at the top of the workflow file — and the trigger you pick decides more than when the workflow fires: it decides what the workflow is safe to be trusted with.

TL;DR — in 30 seconds:

  • A workflow only runs when its on: trigger fires — push, pull_request, schedule, or workflow_dispatch are the four common ones, and a workflow file can list more than one.
  • Branch/tag and path filters narrow a trigger so it doesn't re-run the full pipeline on every push or every file change.
  • A pull_request run can come from a fork's untrusted content — the trigger you pick decides not just when a workflow runs but what it's safe to be trusted with.

1. The common trigger types

Trigger Fires on Typical use
push A commit lands on a branch (or tag) Build/test on every change to your own branches
pull_request A PR is opened/updated against a target branch Check untrusted or in-review changes before merge
schedule A cron expression, on a timer Nightly builds, scheduled cleanup, periodic checks
workflow_dispatch A human clicks "Run workflow" Manual, on-demand runs (a release, a one-off job)
flowchart LR
    P["push"] --> W["Workflow run"]
    PR["pull_request"] --> W
    S["schedule (cron)"] --> W
    M["workflow_dispatch (manual)"] --> W

A workflow file can list more than one trigger — it's common for the same workflow to run on both push and pull_request, with the two paths behaving slightly differently (see §3).

2. Narrowing a trigger

Left unfiltered, on: push fires on every push to every branch. Two filters narrow that:

  • Branch/tag filters — restrict the trigger to specific branches (e.g. only main, or only release branches) or tags, so a workflow doesn't run on every scratch branch a developer pushes.
  • Path filters — restrict the trigger to pushes that touch specific files or directories, so a change to documentation doesn't re-run a build/test pipeline that only cares about application code.

Both exist for the same reason: running a full pipeline on every single push is wasteful and slows down everyone's feedback loop. Narrow the trigger to exactly the changes that matter.

3. Why the trigger changes what you can trust

push and pull_request look similar but carry a real difference: a pull_request can come from a fork — someone else's copy of the repo, whose contents you don't control. A workflow triggered by a pull request from a fork should not be handed the same secrets or write access a push to your own branch gets, because that would let an untrusted contributor exfiltrate a secret just by opening a PR. This is exactly why "which trigger" is a security question, not just a timing one — it's covered operationally in Secrets & environments.

4. Manual and scheduled runs

workflow_dispatch exists for workflows a human should kick off deliberately — a production release, a data-migration job — rather than have fire automatically on every push. schedule (using standard cron syntax) exists for work that isn't tied to a code change at all: a nightly rebuild, a periodic dependency check. Neither needs a code change to trigger; both still run the same jobs/steps machinery underneath.

5. How this connects

  • Once a trigger fires, what happens next — how the workflow's jobs and steps actually execute — is covered in the next page, Jobs, steps & runners.
  • The trust distinction in §3 is the reason Secrets & environments scopes secrets the way it does, and why an environment's protection rules matter most on the trigger that leads to a production deploy.

Common gotchas

  • Treating a pull_request trigger as equally trustworthy as push. Fix: a pull_request can come from a fork you don't control — never hand it the same secrets or write access a push to your own branch gets.
  • Leaving on: push unfiltered. Fix: without branch/tag or path filters, every push to every branch — even a documentation-only change — re-runs the full pipeline; narrow the trigger to the changes that matter.
  • Assuming schedule or workflow_dispatch needs an accompanying commit to fire. Fix: neither does — a scheduled run and a manual "Run workflow" click both start the same jobs/steps machinery with no code change involved.
  • Forgetting a workflow file can list more than one trigger. Fix: the same file commonly runs on both push and pull_request, and each path can behave differently — check which trigger actually fired before assuming why a run started.
Check yourself — why shouldn't a workflow triggered by pull_request from a fork get the same secrets as one triggered by push to your own branch?

Fork content is untrusted — anyone can open a pull request from their own copy of the repo. If that trigger got the same secrets/write access a push does, an attacker could exfiltrate a secret just by opening a PR containing a workflow step that reads it.

Check yourself — a workflow re-runs on every single push, including documentation-only commits. What's the fix, and why not just delete the trigger?

Add a path filter to on: push so it only fires on pushes that touch the files the pipeline actually cares about. Deleting the trigger entirely would also skip legitimate code changes bundled in the same push as a doc edit — filtering narrows the trigger, it doesn't remove it.

You can defend this when you can pick the right trigger for a given scenario (routine build vs. PR check vs. nightly job vs. manual release) and explain, unprompted, why a pull_request from a fork shouldn't be trusted the same as a push to your own branch.