Workflow Triggers, Filters, Expressions, Contexts, Variables, Inputs, and Outputs: Concepts, Architecture, and Mental Model
A workflow can be perfectly valid YAML and still run at the wrong time, skip required validation, consume the wrong value, or turn attacker-controlled text into shell syntax. This lesson treats activation and data flow as an execution contract: what event is allowed to start work, which data exists at each evaluation point, and how values cross step/job/workflow boundaries.
Learning objectives
-
Explain why trigger eligibility and in-workflow
if:conditions are different control planes. -
Predict how activity types, branch/tag filters, path filters,
schedules,
workflow_dispatch,workflow_call, andrepository_dispatchchoose executions. - Use GitHub expression literals, operators, functions, truthiness, loose equality, and explicit conversion without confusing expression evaluation with shell evaluation.
-
Distinguish the
github,env,vars,secrets,inputs,steps,needs,matrix,runner, andjobcontexts by scope and availability. - Model environment variables, configuration variables, secrets, and step/job/workflow outputs as different data classes with different trust and lifetime rules.
Availability: The mandatory path uses GitHub.com, GitHub Free, a disposable public personal repository, and standard GitHub-hosted runners. Organization/environment variables and secrets have additional ownership/policy rules; reusable workflows and external dispatch are introduced here without requiring an organization or paid plan.
1. The problem: “valid YAML” is not a reliable activation policy
Chapter 13 established the execution chain: an event selects a workflow definition, GitHub creates a run, jobs are scheduled, runners execute steps, and each job receives a scoped identity. Chapter 14 focuses on the two questions that decide whether that chain is trustworthy: when is the workflow allowed to activate? and where did every value used by that execution come from?
A common failure is to blur trigger filtering with job conditionals.
A branch/path filter is evaluated before a run is created. A
job-level if: is evaluated after the workflow run
exists. The operational consequences differ: one saves execution
entirely; the other creates a run whose job may be skipped. When
required checks are involved, a skipped workflow caused by filters
can also leave a required check pending, so filter design is part of
branch governance.
2. Mental model: activation gate → expression gate → execution → data contract
flowchart TD
E["Event + payload"] --> F["Trigger filters"]
F -->|eligible| R["Workflow run"]
F -->|not eligible| N["No run"]
R --> X["Expressions + contexts"]
X --> J["Jobs / steps"]
J --> O["Step output"]
O --> JO["Job output"]
JO --> D["needs consumer"]
V["env / vars / secrets / inputs"] --> X
Trigger filters decide whether a run exists. Expressions and contexts then decide job/step behavior. Outputs are explicit contracts that move small values forward; they do not retroactively change trigger eligibility.
Every arrow has a different trust meaning. Event payload is input to
GitHub’s scheduler. Context values are structured data exposed to
expression evaluation. When you inject a context value into an
inline run: block, GitHub first renders a script and
then the runner’s shell parses it; that is exactly where untrusted
event text can become code. Passing untrusted text through an
environment variable keeps it data for the shell command to read,
rather than generating shell source code.
3. Trigger families and what state they bind to
| Trigger | Primary use | Important identity / rule |
|---|---|---|
Repository events (push,
pull_request, issues, releases)
|
Automatic response to GitHub activity | Event payload determines ref/SHA; activity types can narrow some events. |
workflow_dispatch |
Operator-initiated run with typed inputs | Workflow file must exist on default branch to receive dispatch; caller can select a ref for the run. |
workflow_call |
Reusable workflow contract | Called by another workflow; typed inputs are boolean/number/string and outputs map from called jobs. |
repository_dispatch |
External system asks GitHub to create a custom repository event |
Workflow must exist on default branch; run SHA/ref identify
latest default-branch commit; client_payload is
input data, not trusted code.
|
schedule |
Time-based recurring execution | Runs latest commit on default branch; current GitHub.com syntax supports POSIX cron and optional IANA timezone. |
Activity type is a subtype of an event such as
issue opened versus labeled. If several
configured activity types happen, each event can create its own run.
A workflow is not a singleton scheduler unless you add concurrency
semantics later.
4. Branch, tag, and path filters are predicates—not suggestions
GitHub’s current workflow syntax makes the combination rule
explicit: when the same event has both a branch filter and a path
filter, both must be satisfied. For
pull_request, the branch filter evaluates the
target/base branch. For push, branch or tag patterns
evaluate the pushed ref. Path filters are not evaluated for tag
pushes.
on:
push:
branches:
- 'release/**'
paths:
- 'service/**'
- '!service/docs/**'
This run exists only when the push is to a matching
release/** branch and the changed-file
calculation leaves at least one matching service path after ordered
exclusions. Pattern order matters when using !. GitHub
also computes changed-file sets differently for pushes and pull
requests, which is why a filter that looks intuitive can surprise
you on a large or newly created branch.
Required-check interaction: If a workflow is skipped because of branch/path filtering and that workflow is configured as a required check, GitHub documents that the associated check can remain Pending and block merging. Required checks therefore need a trigger design that always creates the expected check in governed cases.
5. Typed inputs: preserve intent at the boundary
workflow_dispatch currently supports
boolean, choice, number,
environment, and string. The
inputs context preserves Boolean values as Booleans;
github.event.inputs exposes corresponding event data
but converts values such as booleans to strings. Prefer
inputs when you want typed manual/reusable contracts.
on:
workflow_dispatch:
inputs:
run_extended:
description: Run the extended validation path
type: boolean
required: true
default: false
target:
description: Validation target
type: choice
options: [smoke, full]
default: smoke
Reusable workflow_call inputs are intentionally
narrower: boolean, number, or
string. Treat the declared inputs as an API schema for
automation callers. Passing an undeclared reusable-workflow input is
an error rather than silently creating a new variable.
6. Expressions are a language with coercion rules
GitHub expressions are evaluated by GitHub, not by Bash or
PowerShell. They have literals, property/index access, comparison
operators, logical operators, and functions such as
contains, startsWith,
fromJSON, toJSON, and status functions.
GitHub currently performs loose equality: different
types may be coerced to numbers; an empty string converts to zero,
Booleans map to one/zero, and non-numeric strings can become
NaN. String comparisons are case-insensitive.
# Step outputs are strings. Convert explicitly before numeric comparison.
- id: settings
run: echo "retry_count=2" >> "$GITHUB_OUTPUT"
- if: ${{ fromJSON(steps.settings.outputs.retry_count) > 1 }}
run: echo "controlled numeric comparison"
The production habit is simple: if a value crosses a boundary as text—step output, environment variable, event field—convert or validate it before relying on numeric/Boolean semantics. Do not rely on surprising coercion as a feature.
7. Contexts are structured namespaces with availability rules
| Context | What it represents | Common boundary |
|---|---|---|
github |
Run/event/repository/ref/actor metadata | Many properties are attacker-influenceable; never assume “GitHub context” means trusted. |
env |
Workflow/job/step environment declarations | Most specific scope wins while a step executes. |
vars |
Non-sensitive configuration variables from repo/org/environment | Values are not masked; unset variables resolve to empty string. |
secrets |
Secrets available to the workflow/job | Availability depends on event, fork, environment, policy; do not intentionally print them. |
inputs |
Manual or reusable-workflow typed inputs | Only populated for applicable invocation types. |
steps |
Prior step outcomes/outputs in the same job | Requires step id; outputs are strings. |
needs |
Direct prerequisite jobs and their outputs/results |
Only jobs named in needs are exposed directly.
|
matrix |
Current matrix combination | Exists for matrix-expanded jobs. |
runner |
Current runner information | Available after runner selection/execution context exists. |
job |
Current job status/services metadata | Job-scoped execution state. |
Context availability is part of the schema. A context that exists in one key can be illegal or empty in another. The official context-availability table is the source of truth when a workflow parser says a context is not allowed where you used it.
8. env, vars, secrets, inputs, and outputs solve different problems
env is configuration embedded in one
workflow file and scoped to workflow/job/step.
vars is non-sensitive configuration
managed at repository, organization, or environment scope.
secrets is sensitive configuration
with restricted exposure and log masking as a best-effort
protection—not a license to print values.
inputs are caller-supplied parameters
with an explicit invocation contract. outputs are
producer-supplied results consumed by later execution units.
A useful question is “who owns this value?” Source-controlled
workflow defaults belong in env;
organization/repository operational configuration belongs in
vars; credentials belong in secrets or short-lived
federation; operator choices belong in inputs; calculated results
belong in outputs.
9. Outputs create explicit forward-only contracts
A step writes a small value to the file named by
GITHUB_OUTPUT. A later step in the same job reads
steps.<id>.outputs.<name>. To cross a job
boundary, map that step output into
jobs.<job>.outputs and make the consumer declare
needs.
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
mode: ${{ steps.derive.outputs.mode }}
steps:
- id: derive
run: echo "mode=smoke" >> "$GITHUB_OUTPUT"
consume:
needs: prepare
runs-on: ubuntu-latest
steps:
- env:
MODE: ${{ needs.prepare.outputs.mode }}
run: printf 'mode=%s\n' "$MODE"
Outputs are best for small metadata such as a version string, Boolean decision, digest, or identifier. Files should move through artifacts/cache/package systems, which have their own retention, integrity, and trust properties covered later in the course.
10. Inspect current workflow state before adding activation logic
Before a lab—or before modifying production automation—inspect what GitHub already knows. This distinguishes “no workflow exists,” “workflow exists but disabled,” and “workflow exists but never received a matching event.”
REPO="OWNER/REPOSITORY"
gh workflow list -R "$REPO" --all --json id,name,path,state
gh run list -R "$REPO" --limit 10 --json databaseId,workflowName,event,headBranch,headSha,status,conclusion,url
gh variable list -R "$REPO" --json name,value,updatedAt
Do not use gh secret list to “debug” by exposing
values—it does not reveal secret values anyway, and the correct
question is whether a secret is configured/available to the event
and environment, not what its plaintext is.
11. Why this matters in DevOps
Trigger correctness determines whether validation exists at the moment change-control policy expects it. Data-flow correctness determines whether a deployment, report, or release acts on the intended target. A workflow that accidentally runs with a production input on an untrusted event is not a YAML mistake; it is a production control failure.
12. Lesson summary
Triggers decide whether a run exists; expressions decide which in-run paths execute; contexts expose structured state at specific evaluation points; variables/secrets/inputs classify configuration and caller data; outputs define explicit forward data contracts. Chapter 14 will now make those rules observable with a disposable workflow.
Knowledge check
A push trigger has both
branches: [release/**] and
paths: [service/**]. Which condition is enough to
start a run?
Neither alone. GitHub requires both branch and path filters to be satisfied when both are configured for the event.
Why prefer inputs.run_extended to
github.event.inputs.run_extended for a Boolean
manual input?
The inputs context preserves the Boolean type,
while the event-input representation converts values to strings.
Why can comparing a step output numerically be surprising?
Step outputs are strings. GitHub expressions use loose coercion;
explicitly convert with fromJSON() or validate the
value before numeric comparison.
Is vars.DEPLOY_KEY an appropriate place for a
password?
No. Configuration variables are non-sensitive and can appear unmasked. Sensitive credentials belong in secrets or, preferably where applicable, short-lived federated credentials.
A downstream job reads needs.build.outputs.digest.
What must be true?
The downstream job must directly declare
needs: build, and the build job must declare an
output named digest mapped from a producer
step/output.
Further reading — current official GitHub sources
- GitHub Docs — Workflow syntax
- GitHub Docs — Events that trigger workflows
- GitHub Docs — Expressions
- GitHub Docs — Contexts reference
- GitHub Docs — Variables
- GitHub Docs — Pass job outputs
- GitHub Docs — Script injections
- GitHub Docs — Secure use reference
- GitHub CLI — gh workflow run
- GitHub CLI — gh run list
- GitHub Docs — repository_dispatch event
- GitHub Docs — Secrets in Actions
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.