1.05 Using Multiple Triggers (Events)
Overview
A workflow is not limited to one event. Listing several under on: lets the same pipeline react to
automatic activity and to manual runs.
Abstract
The shortest form is a YAML array: on: [push, workflow_dispatch]. Any listed event starts the
workflow, and there is no limit on how many you add. Adding workflow_dispatch is the common
case — it keeps the automatic push trigger while also exposing a Run workflow button in the
Actions tab. Each trigger produces an independent run, so a manual run and a push-triggered run
can queue and execute at the same time.
Why It Matters in Production
Every pipeline eventually needs a manual escape hatch. During an incident you want to re-run a
deploy without pushing an empty commit, and workflow_dispatch is that switch.
Multiple triggers also mean multiple concurrent runs of the same workflow. On a test pipeline that
is harmless; on a deploy pipeline two overlapping runs can race each other onto the same target. The
trigger list and a concurrency policy are decisions you make together.
Key Concepts
| Concept | Meaning |
|---|---|
| Array form | on: [push, workflow_dispatch] — compact list of event names |
| Mapping form | on: with each event as a key, required once you add filters or types: |
| Independent runs | Each triggering event creates its own run with its own log and status |
| Run workflow button | Appears in the Actions tab once workflow_dispatch is listed |
| Queued runs | Runs wait for an available runner; several can be in flight simultaneously |
github.event_name |
Context value telling a step which event started the run |
Note
The array form only works while every event is bare. The moment one event needs configuration
(branches:, paths:, types:, cron:), switch the whole block to the mapping form.
Common Use Cases
[push, workflow_dispatch]— automatic CI plus an on-demand re-run[push, pull_request]— validate branch commits and incoming PRs with one pipeline[workflow_dispatch, schedule]— nightly job that can also be kicked off by hand[push, repository_dispatch]— pipeline triggered by commits or by an external system[release, workflow_dispatch]— publish on a GitHub release, with a manual retry path
Example Configuration or Commands
Two triggers on the deployment workflow — pushes run it automatically, the button runs it on demand:
# .github/workflows/deployment.yml
name: Deploy Project
on: [push, workflow_dispatch]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v4
- name: Install NodeJS
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v4
- name: Install NodeJS
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Deploy
run: echo "Deploying..."
Preventing overlapping runs — important once a workflow deploys:
# Cancels an in-flight run of the same workflow on the same ref when a new one starts
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Triggering and observing runs from the CLI:
# Trigger the workflow_dispatch event without opening the browser
gh workflow run deployment.yml --ref main
# See which event started each run
gh run list --limit 10 --json displayTitle,event,status,conclusion
# Watch runs that are currently queued or running
gh run list --status in_progress
gh run watch
Tip
The Actions tab shows a banner reading that the workflow has a workflow_dispatch event
trigger, with a Run workflow dropdown that asks which branch to use. It runs that branch's
copy of the file — useful for testing workflow changes before merging them.
Best Practices
Best Practices
- Keep
workflow_dispatchon every meaningful workflow. It costs nothing and is the fastest recovery path during an incident. - Pair multiple triggers with a
concurrencygroup so overlapping runs cannot race. - Narrow the automatic triggers (
branches:,paths:) as soon as the workflow does real work; leaveworkflow_dispatchunfiltered as the manual override. - Use
github.event_namein anif:rather than splitting one pipeline into near-identical workflow files per trigger. - Add
inputs:toworkflow_dispatchwhen the manual case needs parameters (target environment, version tag) instead of hardcoding them. - Switch to the mapping form early — array form is fine until the first filter, then it has to be rewritten anyway.
Security Best Practices
Security
workflow_dispatchis available to anyone with write access to the repository. On a workflow that deploys, put the deploy job behind a GitHub Environment with required reviewers.- Restrict dispatch-triggered deploys by actor or branch where it matters:
- Never add
pull_request_targetorissue_commentto the trigger list of a workflow that has secrets in scope — those events run with repository secrets while handling untrusted input. - Treat
workflow_dispatchinputs as untrusted. Pass them viaenv:and quote them; never interpolate${{ inputs.* }}directly into a shell command. - Keep least-privilege
permissions:regardless of trigger — the token scope does not change because a human started the run. - Audit who dispatched a run: the Actions list records the actor, and the audit log keeps the history.
Do and Don't
| ✅ Do | ❌ Don't |
|---|---|
Add workflow_dispatch alongside automatic triggers |
Force an empty commit to re-run a pipeline |
| Use array form while every event is bare | Try to attach branches: inside the array form |
Set a concurrency group on deploy workflows |
Let a manual run race a push-triggered run |
| Gate dispatch-triggered deploys with an Environment | Assume manual means authorized |
Use github.event_name to branch behavior |
Duplicate the workflow once per trigger |
Quote and pass dispatch inputs through env: |
Interpolate inputs into run directly |
Common Mistakes
Common Mistakes
- Filters inside the array form.
on: [push: {branches: [main]}, workflow_dispatch]is invalid YAML — filters require the mapping form. - No Run workflow button. The workflow version containing
workflow_dispatchis not on the default branch yet. - Duplicate runs per PR. Listing both
pushandpull_requestunfiltered runs the pipeline twice for every commit on a PR branch; filterpushtomain. - Overlapping deploys. Two triggers, no
concurrency— an older run finishes last and overwrites the newer deployment. - Expecting event payload fields to exist on every trigger.
github.event.pull_requestis empty on aworkflow_dispatchrun; guard ongithub.event_namefirst. - Assuming dispatch uses the default branch. It runs the file from the branch you select in the dropdown.
Breaks in production
Manually dispatching a deploy while a push-triggered run of the same workflow is still in flight means two pipelines write to the same target. Whichever finishes second wins, which may be the older commit.
Troubleshooting
# Confirm the registered triggers for a workflow
gh workflow view deployment.yml --yaml | head -5
# Which event produced each run, and who started it?
gh run list --limit 10 --json event,displayTitle,status --jq '.[] | "\(.event)\t\(.status)\t\(.displayTitle)"'
# Validate the on: block parses as expected
yq '.on' .github/workflows/deployment.yml
| Symptom | Likely cause |
|---|---|
| "Run workflow" button missing | workflow_dispatch absent, or file not on the default branch |
| "Invalid workflow file" after adding events | Filters used inside the array form |
| Workflow runs twice per commit | Both push and pull_request match the same ref |
| Two runs executing at once | Expected with multiple triggers — add concurrency to serialize |
| Runs stuck in "Queued" | No available runner, or concurrency limits reached |
| Step reads an empty event field | That field does not exist for the triggering event |
Quick Recap
on: [push, workflow_dispatch]is the compact array form for multiple bare events.- Any listed event triggers the workflow; there is no limit on how many you list.
workflow_dispatchadds the manual Run workflow button in the Actions tab.- Each trigger creates an independent run, so runs can queue and execute simultaneously.
- The mapping form is required as soon as any event needs filters or activity types.
- Use
concurrencyto stop overlapping runs, especially on deploy workflows. - Triggers can be refined further by branch, path, and tag — covered separately.
Interview / Revision Notes
Quick revision
Q: How do you give a workflow several triggers?
List them under on: — array form on: [push, workflow_dispatch], or mapping form when filters
are needed.
Q: What does adding workflow_dispatch give you?
A manual Run workflow button in the Actions tab, with a branch selector.
Q: Can a manual run and a push-triggered run happen at once? Yes — each event produces an independent run, and both can be queued or running together.
Q: When must you use the mapping form instead of the array?
As soon as any event needs configuration: branches, paths, types, or cron.
Q: How does a step know which event started the run?
${{ github.event_name }}.
Q: How do you stop overlapping runs of the same workflow?
A concurrency group, usually keyed on workflow and ref, with cancel-in-progress: true.
Q: Which branch does a dispatched run use?
The one chosen in the Run workflow dropdown (or --ref with the CLI).
This topic can be expanded into sub-pages: branch, path and tag filters; activity types per event;
workflow_dispatchinputs; concurrency groups; and conditional jobs driven bygithub.event_name.