Chapter 04Lesson 01~120 minutes

Contexts, Expressions, Functions, Conditionals, and Evaluation Semantics: Core Concepts and Mental Model

Chapters 01–03 established how an event selects a workflow and creates a run. Chapter 04 now opens the evaluation engine inside that run: GitHub builds typed context objects, evaluates expressions at specific lifecycle phases, and uses the results to decide job admission, step execution, configuration values, and dataflow. The goal is not to memorize punctuation; it is to be able to explain exactly which value existed, where it was available, what type/coercion applied, and why a condition evaluated the way it did.

ContextsExpressionsTypesLifecycle phasesStatus semantics

Learning objectives

  • Model expression evaluation as typed dataflow from event/configuration/prior-result state into job and step behavior.
  • Distinguish major contexts such as github, inputs, vars, env, job, runner, steps, and needs by lifecycle availability.
  • Explain literals, truthiness, loose equality, missing properties, and why string-valued outputs often require explicit conversion.
  • Recognize the difference between GitHub expression evaluation and later shell/environment expansion on the runner.
  • Inspect only bounded context fields needed for evidence instead of dumping sensitive or attacker-controlled objects.

1. The practical problem: “Why did this condition run?”

Trigger design tells you why a workflow run exists. Expression design tells you why the run took one path instead of another. A production incident often sounds like “the deploy job unexpectedly ran,” “the cleanup step never executed after failure,” or “the boolean input was false but the condition was true.” Those are not generic YAML problems. They are dataflow problems.

To diagnose them, preserve four facts: the expression source, the contexts available at that workflow key, the runtime value and type, and the status state that GitHub implicitly or explicitly adds. Without those facts, changing punctuation until the workflow turns green is guesswork.

2. Mental model: state becomes contexts; contexts become decisions

Expression evaluation across lifecycle phases
flowchart TD
  A[Event + workflow metadata] --> B[Early contexts: github inputs vars needs]
  B --> C[Expression evaluation]
  C --> D[Job admitted or skipped]
  D --> E[Runner assigned]
  E --> F[Runtime contexts: runner job steps env]
  F --> G[Step expressions + shell environment]
  G --> H[Outputs conclusions evidence]

The important boundary is between GitHub-side evaluation and runner-side execution. A job-level if can be evaluated before any runner is assigned, so it cannot depend on runner-only state such as runner.os. A step-level condition runs later, when the job and previous step records exist. The same expression syntax therefore does not imply the same data is available everywhere.

3. Contexts are typed objects with scope, not global dictionaries

Context What it represents When it becomes useful Beginner pitfall
github Run/event/workflow/repository metadata. Available broadly, including many pre-run expressions. Treating every field as trusted or dumping the whole object.
inputs Typed manual/reusable-workflow inputs. workflow_dispatch or reusable workflows. Replacing it with github.event.inputs and losing Boolean typing.
vars Configuration variables. Policy/configuration values available before runner execution in many keys. Confusing configuration variables with secrets.
env Workflow/job/step environment values defined in workflow configuration. Step expressions and runtime shell environment. Assuming it contains every default runner variable.
needs Direct dependency job results/outputs. Downstream jobs after dependency evaluation. Assuming it contains every transitive ancestor automatically.
runner Assigned runner state such as OS/arch/temp paths. After a job is routed to a runner. Using it in a job-level if where it is unavailable.
job Current job state. During job/step execution. Using final job status before the job has reached that state.
steps Earlier/current named step outcomes, conclusions, and outputs. After referenced steps have executed. Referencing an output before its producer step or forgetting outputs are strings.

The authoritative context-availability table in GitHub documentation is part of your production contract. When a context is not listed for a workflow key, changing quotation marks will not make it available.

4. Expressions have types, truthiness, and coercion

GitHub expressions support Boolean, null, number, and string literals. In a conditional, documented falsy values—false, numeric zero, empty string, and null—become false; other values are truthy. This is why the non-empty string 'false' is dangerous when someone assumes it behaves like the Boolean false.

Equality is intentionally loose. If operand types differ, GitHub may coerce them to numbers. For example, null becomes 0, Boolean true becomes 1, empty string becomes 0, and a non-numeric string can become NaN. Relational comparisons involving NaN are false. A workflow that depends on accidental coercion is difficult to review; normalize types explicitly instead.

Source Typical type Safer interpretation
inputs.enable_demo from Boolean manual input Boolean Use directly: if: inputs.enable_demo.
github.event.inputs.enable_demo String representation Prefer inputs when available.
steps.producer.outputs.count String Use fromJSON(...) before numeric comparison.
Missing context property Empty string Test existence deliberately; do not confuse missing with Boolean false.
env.RETRY_LIMIT String Convert with fromJSON when a numeric/Boolean workflow field needs a typed value.

5. Expression syntax and shell syntax are two different languages

${{ ... }} asks GitHub to evaluate an expression. A shell variable such as $NAME or ${NAME} is expanded later by the shell running on the assigned runner. Mixing the two phases can create both correctness and security bugs.

jobs:
  explain:
    if: ${{ github.ref == 'refs/heads/main' }}   # GitHub evaluates this first
    runs-on: ubuntu-24.04
    steps:
      - name: Runtime expansion
        env:
          SAFE_REF: ${{ github.ref }}             # GitHub copies data into env
        run: |
          printf 'ref=%s\n' "$SAFE_REF"           # Bash expands quoted env value

In an if condition the outer ${{ }} delimiters are usually optional. Elsewhere, use them where the workflow syntax expects an expression. Keep expression literals and shell quoting rules mentally separate.

6. Functions transform data; they do not erase type questions

Useful built-ins include contains, startsWith, endsWith, format, join, toJSON, fromJSON, hashFiles, and current case selection syntax. Availability can depend on the workflow key; for example, hashFiles operates on files in GITHUB_WORKSPACE and is not a universal pre-run function.

env:
  MODE: ${{ case(inputs.verbose, 'verbose', 'normal') }}
  SAFE_LIST: ${{ join(fromJSON('["lint","test"]'), ',') }}

Do not use toJSON as permission to dump entire contexts. It is a serialization function, not a redaction boundary. Serialize only synthetic or explicitly allow-listed values.

7. Every conditional also has a status dimension

For if expressions, GitHub applies a default status check of success() unless your condition includes a status-check function. That means a step after a failure may not run even if its business expression is true.

Function Meaning Typical use
success() Previous relevant work has succeeded. Normal happy-path steps.
failure() A previous step/job in the relevant chain failed. Failure diagnosis/evidence.
cancelled() The workflow was cancelled. Cancellation-specific handling.
always() Evaluates true even after cancellation/failure. Narrow step-level evidence tasks; not a universal cleanup hammer.
!cancelled() Run unless cancellation occurred. Often safer for evidence/cleanup that should follow success or failure but not cancellation.

8. Read-only inspection: allow-list context fields

name: Chapter 04 context probe
on:
  workflow_dispatch:
    inputs:
      enabled:
        description: 'Enable the probe'
        type: boolean
        required: true
        default: true
permissions: {}
jobs:
  inspect:
    if: ${{ inputs.enabled }}
    runs-on: ubuntu-24.04
    steps:
      - name: Bounded context evidence
        env:
          EVENT: ${{ github.event_name }}
          REF: ${{ github.ref }}
          SHA: ${{ github.sha }}
          WORKFLOW_SHA: ${{ github.workflow_sha }}
          RUNNER_OS: ${{ runner.os }}
          RUNNER_ARCH: ${{ runner.arch }}
        run: |
          printf 'event=%s ref=%s sha=%s workflow_sha=%s\n' "$EVENT" "$REF" "$SHA" "$WORKFLOW_SHA"
          printf 'runner_os=%s runner_arch=%s run=%s attempt=%s\n' \
            "$RUNNER_OS" "$RUNNER_ARCH" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT"

This is enough to prove event/workflow/runner identity without printing the whole github, secrets, or event payload objects.

9. Production rule: make conditions reviewable as dataflow

A reviewer should be able to read a condition and answer: where did every value come from, what type does it have, at which lifecycle phase is it available, is any field attacker-controlled or sensitive, what status function is implicit/explicit, and what observable state changes if the condition is true? If those questions cannot be answered, the condition is too magical.

Next lesson

Guided context and expression workflow

Build one controlled workflow that exposes typed manual inputs, safe context inspection, string outputs, explicit conversion, conditionals, and status behavior after an intentional failure.

Knowledge check

Why can runner.os work in a step but not in a job-level if?

A step output contains "5". Why should a numeric comparison use fromJSON?

What does a nonexistent context property evaluate to?

Why is the string "false" risky in an if?

A failure-diagnostic step has if: steps.test.conclusion == 'failure' but never runs after the test fails. What is missing?

Official references and version notes

  • Evaluate expressions in workflows and actions — current literals, truthiness, loose equality/coercion, built-in functions, fromJSON/toJSON, and status-check functions.
  • Contexts reference — current context objects, missing-property behavior, per-key context availability, and steps/runner/inputs semantics.
  • Workflow syntax for GitHub Actions — current if, workflow_dispatch input types, environment scopes, job/step syntax, and permission placement.
  • Script injections — why attacker-controlled context text must not be substituted directly into generated shell scripts.
  • Secure use reference — current safe handling guidance for secrets, untrusted context values, permissions, and workflow code.
  • Variables reference — distinction between default runner environment variables and expression contexts.
Version and compatibility note

Version-sensitive expression/context 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: {}; no external action, secret, package, deployment, self-hosted runner, or cloud account is required. At verification time, expression literals include booleans/null/numbers/strings; conditionals coerce documented falsy values to false; equality is loose and can numerically coerce mismatched types; nonexistent context properties evaluate to an empty string; steps.*.outputs.* values are strings; manual-workflow values in the inputs context preserve Boolean typing while github.event.inputs represents Boolean values as strings; and a default success() status check applies to if expressions unless a status-check function is present. Current docs also caution against broad always() use for critical tasks and recommend !cancelled() when appropriate. GitHub Actions is continuously delivered, so these semantics and context-availability tables must be rechecked when regenerating the lesson. The chapter intentionally uses no third-party action so context/expression semantics remain visible without action-runtime or supply-chain dependencies.

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.