# GitHub Actions Triggers: Every on: Event and Its Traps

> Every GitHub Actions trigger event with the scoping rules that decide whether a workflow runs, including the default-branch requirement that silently disables schedules and dispatch.

Source: https://latchkey.dev/learn/github-actions/github-actions-workflow-triggers-reference  
Updated: 2026-08-20

Two rules cause most "my workflow did not run" reports: schedules and manual dispatch only work from the default branch, and path or branch filters are evaluated before anything else.

A workflow that does not run produces no log to read, which makes trigger problems disproportionately annoying to debug. Nearly all of them come down to a small set of scoping rules.

The most surprising: a `schedule` or `workflow_dispatch` workflow must exist on the **default branch** before GitHub will offer or fire it at all. Testing one on a feature branch does not work and gives no feedback.

## The events

| Event | Fires when | Watch for |
| --- | --- | --- |
| `push` | Commits pushed | Branch and path filters apply |
| `pull_request` | PR opened, synchronised, reopened | Fork PRs get a read-only token, no secrets |
| `pull_request_target` | Same, but in base repo context | Has secrets. Never check out untrusted PR code |
| `workflow_dispatch` | Manual run | Must be on the default branch to appear |
| `schedule` | Cron | Default branch only; UTC; best-effort timing |
| `workflow_call` | Called by another workflow | Inputs are strictly typed |
| `workflow_run` | Another workflow completes | Runs in default-branch context |
| `release` | Release published or edited | Draft releases do not fire `published` |
| `issue_comment` | Comment on issue or PR | Fires for both; check `.issue.pull_request` |
| `repository_dispatch` | External API call | Requires a token with `contents: write` |

## Filters and how they combine

```.github/workflows/ci.yml
on:
  push:
    branches: [main, 'release/**']
    paths: ['src/**', '!src/**/*.md']
  pull_request:
    branches: [main]
    paths-ignore: ['docs/**']
```

> `branches` and `paths` are ANDed: both must match. You cannot use `branches` and `branches-ignore` on the same event, nor `paths` with `paths-ignore`. Negation goes inside the positive list with `!`.

## The traps worth memorising

- **Schedules and dispatch require the default branch.** The workflow file must be on it, or the schedule never fires and the Run workflow button never appears.
- **`pull_request_target` runs with secrets in the base context.** Checking out the PR head under it executes untrusted code with your secrets. This is the most dangerous misconfiguration in Actions.
- **Path filters do not apply to `workflow_dispatch`.** A manual run always executes regardless of what changed.
- **Scheduled runs are best-effort** and are commonly delayed during peak load. Do not rely on precise timing.
- **A scheduled workflow is disabled after 60 days** of repository inactivity.
- **`GITHUB_TOKEN` events do not trigger other workflows,** which prevents loops and surprises anyone building a chain.

## Why did it not run?

```Terminal
# was the run even created?
gh run list --workflow=ci.yml --limit 5

# is the file on the default branch? (required for schedule/dispatch)
git ls-tree origin/main -- .github/workflows/ci.yml

# validate the trigger block parses
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color
```

> No run in the list at all means the trigger did not match, so look at filters and branch scope. A run that exists but skipped every job is a job-level `if:` problem instead, which is a different page.

## Diagnose it: print the context before you change anything

Most workflow-expression bugs are not syntax errors, they are an expression reading something that is empty. GitHub resolves a missing property to an empty string instead of failing the run, so a wrong reference looks like a logic bug rather than a mistake. Dump the contexts first and you will usually see the answer immediately.

```.github/workflows/ci.yml
- name: Dump contexts
  run: |
    echo '--- github ---'   ; echo '${{ toJSON(github) }}'
    echo '--- needs ---'    ; echo '${{ toJSON(needs) }}'
    echo '--- steps ---'    ; echo '${{ toJSON(steps) }}'
    echo '--- matrix ---'   ; echo '${{ toJSON(matrix) }}'
    echo '--- inputs ---'   ; echo '${{ toJSON(inputs) }}'
```

> An empty `{}` or a blank line is the finding. It means the context is not populated at that point, which is a different problem from the value being wrong, and it needs a different fix.

## Check the context is allowed where you used it

Contexts are not available everywhere. The same expression can be valid in a step `if` and invalid in a job `if`, which is why an expression that works in one workflow fails when moved.

| Where you wrote it | Contexts available there |
| --- | --- |
| `run-name` | `github`, `inputs`, `vars` |
| `concurrency` | `github`, `inputs`, `vars` |
| Top-level `env` | `github`, `secrets`, `inputs`, `vars` |
| `jobs.<id>.if` | `github`, `needs`, `vars`, `inputs` |
| `jobs.<id>.steps.if` | `github`, `needs`, `strategy`, `matrix`, `job`, `runner`, `env`, `vars`, `steps`, `inputs` |
| `jobs.<id>.outputs` | Full access, including `secrets` |
| Reusable workflow `outputs` | `github`, `jobs`, `vars`, `inputs` |

> The most common trap in this table: `steps` and `matrix` are available in a **step** `if` but not in a **job** `if`. Moving a condition up a level silently breaks it.

## Catch it before it reaches CI

Every failure in this cluster is statically detectable. `actionlint` parses workflow expressions, checks context availability against the same rules above, and validates `needs` references, so these bugs never need to cost you a run.

```Terminal
# one-off
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color

# as a job, before anything expensive runs
- uses: actions/checkout@v4
- run: |
    bash <(curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
    ./actionlint -color
```

## FAQ

### Why is my workflow_dispatch button not showing?

The workflow file must exist on the repository default branch. GitHub reads trigger definitions from the default branch, so a `workflow_dispatch` added only on a feature branch never appears in the UI.

### Why is my scheduled workflow not running?

Most often the file is not on the default branch, which is required for `schedule`. Cron is also UTC and best-effort, so runs are frequently delayed at peak times, and a scheduled workflow is disabled automatically after 60 days of repository inactivity.

### What is the difference between pull_request and pull_request_target?

`pull_request` runs in the context of the merge commit with a read-only token and no secrets for forks. `pull_request_target` runs in the base repository context with secrets available. Never check out untrusted PR code under `pull_request_target`.

### Can I use branches and branches-ignore together?

No, they are mutually exclusive on the same event, as are `paths` and `paths-ignore`. Use negation inside the positive list instead, for example `paths: ['src/**', '!src/**/*.md']`.

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
