Skip to content

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..."
on: [push, workflow_dispatch]
on:
  push:
    branches: [main]        # only deploy from main
  pull_request:
    types: [opened, synchronize]
  workflow_dispatch:
  schedule:
    - cron: '0 3 * * *'
on:
  push:
    branches: [main]
  workflow_dispatch:        # dispatch still works from any branch
jobs:
  deploy:
    # Deploy automatically only on push; manual runs always allowed
    if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Triggered by ${{ github.event_name }}"

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_dispatch on every meaningful workflow. It costs nothing and is the fastest recovery path during an incident.
  • Pair multiple triggers with a concurrency group so overlapping runs cannot race.
  • Narrow the automatic triggers (branches:, paths:) as soon as the workflow does real work; leave workflow_dispatch unfiltered as the manual override.
  • Use github.event_name in an if: rather than splitting one pipeline into near-identical workflow files per trigger.
  • Add inputs: to workflow_dispatch when 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_dispatch is 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:
    if: github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main'
    
  • Never add pull_request_target or issue_comment to the trigger list of a workflow that has secrets in scope — those events run with repository secrets while handling untrusted input.
  • Treat workflow_dispatch inputs as untrusted. Pass them via env: 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_dispatch is not on the default branch yet.
  • Duplicate runs per PR. Listing both push and pull_request unfiltered runs the pipeline twice for every commit on a PR branch; filter push to main.
  • 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_request is empty on a workflow_dispatch run; guard on github.event_name first.
  • 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_dispatch adds 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 concurrency to 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_dispatch inputs; concurrency groups; and conditional jobs driven by github.event_name.