Chapter 14Lesson 01~165 minutes

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.

Actions triggersExpressionsContextsData contracts

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, and repository_dispatch choose 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, and job contexts 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

Concept / workflow diagram
              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?

Why prefer inputs.run_extended to github.event.inputs.run_extended for a Boolean manual input?

Why can comparing a step output numerically be surprising?

Is vars.DEPLOY_KEY an appropriate place for a password?

A downstream job reads needs.build.outputs.digest. What must be true?

Next lesson

Next: Workflow Triggers, Filters, Expressions, Contexts, Variables, Inputs, and Outputs: Guided Hands-On Workflow and Core Operations

Further reading — current official GitHub sources

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.