Chapter 03Lesson 02~145 minutes

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.

Hands-onBranch/path filtersManual dispatchRepository dispatchSchedules

Learning objectives

  • Build a safe event-probe workflow with narrow branch/path filters and no external side effects.
  • Compare push and pull_request evidence for the same repository history.
  • Define typed workflow_dispatch inputs and consume them without unsafe shell interpolation.
  • Exercise repository_dispatch with 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_ref and github.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'
Mutation boundary: this request creates a repository event and can create a workflow run. Use only the explicitly named disposable repository, confirm 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.

Next lesson

Configuration patterns and trade-offs

Choose deliberately between broad/narrow triggers, trigger filters/job conditions, schedules/external orchestration, and manual/custom dispatch.

Knowledge check

A push to feature/probe changes only docs/notes.md. Branch matches, path does not. Run or no run?

Why merge workflow_dispatch configuration to the default branch before testing it?

What ref/SHA should you expect from repository_dispatch?

Why pass dispatch payload values through env before using them in Bash?

Does a cron schedule guarantee execution at exactly 04:17:00?

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.