Events, Triggers, Filters, Schedules, Manual Runs, and Repository Dispatch: Guided Hands-On Workflow
This lesson turns the trigger model into a controlled experiment. One disposable repository gets a small event-probe workflow that is changed in stages. You will predict whether a push, pull request, manual request, or custom repository dispatch should create a run, then reconcile the prediction with the actual event/ref/SHA evidence.
Learning objectives
- Build a safe event-probe workflow with narrow branch/path filters and no external side effects.
-
Compare
pushandpull_requestevidence for the same repository history. -
Define typed
workflow_dispatchinputs and consume them without unsafe shell interpolation. -
Exercise
repository_dispatchwith a synthetic payload in a disposable repository. - Explain why a candidate event created a run, created a skipped job, or created no run at all.
1. Disposable repository and preflight
Use a repository created for this chapter, not a production project.
The examples assume its default branch is main and that
you can create one feature branch and one pull request. No secret,
deployment, package publication, self-hosted runner, or cloud
resource is required.
chapter03-trigger-lab/
app/
message.txt
docs/
notes.md
.github/
workflows/
trigger-probe.yml
Before changing the workflow, record the repository name, default
branch, current commit SHA, and whether the workflow already exists
on main. For manual and repository dispatch,
default-branch presence is part of the trigger contract.
2. Stage A — push and pull-request trigger probe
name: Chapter 03 trigger probe
on:
push:
branches:
- main
- 'feature/**'
paths:
- 'app/**'
- '.github/workflows/trigger-probe.yml'
pull_request:
branches:
- main
paths:
- 'app/**'
- '.github/workflows/trigger-probe.yml'
permissions: {}
defaults:
run:
shell: bash
jobs:
inspect:
runs-on: ubuntu-24.04
env:
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
REF: ${{ github.ref }}
SHA: ${{ github.sha }}
BASE_REF: ${{ github.base_ref }}
HEAD_REF: ${{ github.head_ref }}
steps:
- name: Report bounded event identity
run: |
printf 'event=%s action=%s\n' "$EVENT_NAME" "$EVENT_ACTION"
printf 'ref=%s sha=%s\n' "$REF" "$SHA"
printf 'base=%s head=%s\n' "$BASE_REF" "$HEAD_REF"
printf 'run=%s attempt=%s\n' "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT"
Because both branch and path filters are present, an event must
satisfy both categories. For pull_request.branches, the
branch filter applies to the target/base branch. The path filter
evaluates changed paths. Predict before committing anything.
3. Controlled push cases: prove AND semantics
| Case | Branch | Changed path | Prediction | Reason |
|---|---|---|---|---|
| P1 | feature/probe |
docs/notes.md |
No run | Branch matches, path does not. |
| P2 | feature/probe |
app/message.txt |
Run | Branch and path both match. |
| P3 | main |
app/message.txt |
Run | Both filters match on default branch. |
For P1, “no run” is the evidence. Record the commit SHA and the absence of a new run for this workflow rather than inventing a runner failure. For P2/P3, capture the run ID, event, ref, SHA, and job conclusion.
4. Pull-request case: compare merge context to head context
Create a pull request from feature/probe to
main that changes app/message.txt. The run
should be eligible because the target branch is
main and the changed path matches. Record:
github.ref— expected PR merge ref shape;-
github.sha— merge-branch commit used by the event; -
github.event.pull_request.head.sha— contributor branch head SHA; -
github.base_refandgithub.head_ref.
Add the head SHA to the safe allow-list:
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# ...
- run: printf 'pr_head_sha=%s\n' "$PR_HEAD_SHA"
An empty value outside pull-request events is normal. The field belongs to the event payload, not to every trigger family.
5. Stage B — typed manual dispatch inputs
Merge the workflow to main before testing manual
dispatch. Then add:
on:
workflow_dispatch:
inputs:
target:
description: 'Synthetic target label'
required: true
type: choice
options:
- dev
- test
verbose:
description: 'Emit the optional diagnostic line'
required: true
type: boolean
default: false
Consume the values through environment variables:
env:
TARGET: ${{ inputs.target }}
VERBOSE: ${{ inputs.verbose }}
steps:
- name: Inspect manual request
run: |
printf 'target=%s verbose=%s\n' "$TARGET" "$VERBOSE"
printf 'ref=%s sha=%s\n' "$GITHUB_REF" "$GITHUB_SHA"
The inputs context preserves Boolean input values as
Booleans for expression evaluation; choice resolves to a string. Do
not turn user-supplied strings into shell syntax.
6. Stage C — repository dispatch from a synthetic external signal
repository_dispatch represents “something outside
GitHub told this repository that a custom event happened.” The
workflow must already exist on the default branch. Add:
on:
repository_dispatch:
types: [academy_probe]
Read only a synthetic payload field:
env:
EXTERNAL_SOURCE: ${{ github.event.client_payload.source }}
DRY_RUN: ${{ github.event.client_payload.dry_run }}
steps:
- run: |
printf 'source=%s dry_run=%s\n' "$EXTERNAL_SOURCE" "$DRY_RUN"
printf 'ref=%s sha=%s\n' "$GITHUB_REF" "$GITHUB_SHA"
From an authenticated GitHub CLI session that is authorized for the disposable repository, a guarded example is:
export GH_REPO='OWNER/chapter03-trigger-lab'
gh api --method POST "repos/$GH_REPO/dispatches" \
-f event_type='academy_probe' \
-f 'client_payload[source]=chapter03' \
-F 'client_payload[dry_run]=true'
GH_REPO before sending, and never paste authentication
tokens into the command or lesson file.
For this event, record that the run uses the default branch ref and its current tip SHA, not an arbitrary external commit supplied by the payload.
7. Stage D — schedule design without pretending cron is exact
Add a schedule only after the workflow is on main. This
example deliberately avoids the top of the hour:
on:
schedule:
- cron: '17 4 * * 1-5'
timezone: 'Etc/UTC'
Current GitHub Actions supports POSIX cron, an optional IANA timezone, and a minimum interval of five minutes. Scheduled runs use the latest default-branch commit and can be delayed under service load. For a short course lab, it is acceptable to inspect and reason about the schedule rather than waiting for a production-like periodic run.
8. Mini challenge — choose the right layer
Your team wants docs-only changes to create a visible run but skip
expensive tests. Should you add paths: ['app/**'] at
the trigger or keep a broad trigger and put an
if condition on the expensive job?
The second design is usually the one that preserves a run/check for docs changes. The first prevents run creation entirely for unmatched paths. The “right” answer depends on branch-protection/check policy, cost, and desired visibility—not on YAML terseness.
Knowledge check
A push to feature/probe changes only
docs/notes.md. Branch matches, path does not. Run
or no run?
No run. When branch and path filters are both configured, both categories must match.
Why merge workflow_dispatch configuration to the
default branch before testing it?
Current GitHub behavior requires the manually dispatched workflow file to exist on the default branch before the dispatch trigger is available.
What ref/SHA should you expect from
repository_dispatch?
The default branch ref and the last commit on the default branch, not a commit embedded in the custom payload.
Why pass dispatch payload values through
env before using them in Bash?
Event-derived values are untrusted input; environment variables plus normal shell quoting avoid injecting expression text directly into shell syntax.
Does a cron schedule guarantee execution at exactly 04:17:00?
No. Scheduled workflows can be delayed under GitHub Actions load, so cron expresses desired schedule eligibility rather than a hard real-time guarantee.
Official references and version notes
- Events that trigger workflows — authoritative event-specific ref/SHA, default-branch, activity-type, schedule, and dispatch semantics.
- Triggering a workflow — branch/path filters, manual inputs, event configuration, and trigger examples.
-
Workflow syntax for GitHub Actions
— current
onsyntax, filter combination rules, schedule syntax, and manual input types. -
Contexts reference
— current
githubcontext fields and safe access patterns. -
Variables reference
— current meanings of
GITHUB_REF,GITHUB_SHA, event path, and run identity variables. - Troubleshooting workflows — current guidance for trigger conditions, default-branch requirements, merge-conflict behavior, and schedule delays.
- Manually running a workflow — UI/CLI/API manual dispatch behavior and branch selection.
- REST API endpoints for repository dispatch — current API contract for custom repository events.
Version-sensitive trigger behavior was rechecked against current
primary GitHub documentation on 2026-09-09.
Executable labs use a disposable repository,
ubuntu-24.04, built-in shell steps only, and explicit
permissions: {} because no repository API mutation is
required from inside the workflow. At verification time,
workflow_dispatch, repository_dispatch,
and schedule require the workflow file to exist on
the default branch; scheduled workflows run from the latest
default-branch commit, support POSIX cron plus an optional IANA
timezone, and have a minimum five-minute interval but may be
delayed under load. GitHub is continuously delivered, so event
payload fields, limits, schedule behavior, and default-branch
rules must be rechecked before long-lived production use. The
authenticated gh api example is optional and must
target only the learner-owned disposable repository. No credential
value is ever embedded in workflow source or shell history.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.