Chapter 05Lesson 01~125 minutes

Environment Variables, Configuration Variables, Outputs, and Data Passing: Core Concepts and Mental Model

Chapter 04 taught you to ask what a value means, what type it has, and when it is available. Chapter 05 adds the transport problem: the same logical value can live in workflow YAML, a configuration-variable store, a runner process environment, an environment file, a step output, a job output, or a file artifact. Reliable pipelines make that path explicit so configuration is not mistaken for a secret, transient process state is not mistaken for cross-job state, and small control data is not confused with durable files.

envvarsGITHUB_ENVGITHUB_OUTPUTJob outputs

Learning objectives

  • Classify values by origin, sensitivity, scope, lifetime, and transport mechanism before placing them in a workflow.
  • Explain workflow/job/step env precedence and distinguish the env context from runner environment variables.
  • Explain how vars differs from env and why configuration variables are not a secret store.
  • Use GITHUB_ENV and GITHUB_OUTPUT as per-step environment-file channels with the correct visibility boundary.
  • Choose step output, job output, reusable-workflow output, or artifact/file transfer according to data size, lifetime, and audit needs.

1. The practical problem: one value, many possible homes

Suppose a build needs an application name, a deployment channel, a generated semantic version, and a compiled package. All four are “data,” but they should not travel the same way. The application name is static configuration; the channel may be repository/environment configuration; the generated version is small control data produced at runtime; and the package is a file that may need to cross jobs and remain downloadable.

A common beginner failure is to put all four into environment variables. That works only while one process or one job owns the data. Once a second job starts on a fresh runner, ambient process state disappears. A second failure is the opposite: using artifacts or repository variables for tiny ephemeral values and creating unnecessary coupling.

Chapter rule: before choosing syntax, classify every value by origin → sensitivity → scope → lifetime → size/format → consumer.

2. Mental model: configuration becomes process state, then explicit outputs

Value flow and boundary changes
flowchart TD
  A[Workflow env / vars] --> B[Job configuration]
  B --> C[Runner process environment]
  C --> D[Step writes GITHUB_ENV]
  D --> E[Later steps in same job]
  C --> F[Step writes GITHUB_OUTPUT]
  F --> G[steps.id.outputs]
  G --> H[Job output mapping]
  H --> I[needs.job.outputs in downstream job]
  C --> J[Generated file]
  J --> K[Artifact/file transport when needed]

Each arrow changes either scope or ownership. GITHUB_ENV extends a value only to later steps in the same job. GITHUB_OUTPUT creates a named step interface. Mapping that step output to jobs.<id>.outputs creates a cross-job interface. A file/artifact is a different transport entirely.

3. Six value families you must keep distinct

Mechanism Best for Scope/lifetime Wrong use
env in workflow YAML Non-secret runtime configuration for this workflow/job/step. Workflow, job, or step; materialized for runner steps. Assuming a job-level environment variable exists in another job.
vars configuration variables Reusable non-sensitive organization/repository/environment configuration. Managed outside workflow YAML; availability depends on scope. Passwords, tokens, or opaque large payloads.
Default GITHUB_*/RUNNER_* variables Run/runner identity supplied by GitHub. Every step where documented. Trying to redefine authoritative run identity.
GITHUB_ENV Runtime value needed by later steps in the same job. Subsequent steps in that job only. Cross-job transport.
GITHUB_OUTPUT + step/job outputs Small named control data with explicit producer/consumer contract. Step → later expressions; job output → downstream needs jobs. Large files, binaries, secret dumping.
Files/artifacts Build products, reports, logs, larger structured data. Explicit file transfer/retention path. Tiny Boolean or version value that should be a named output.

4. env has lexical scope and nearest-scope precedence

A custom environment variable can be defined at workflow, job, or step scope. When the same name appears more than once, the most specific active scope wins: step over job over workflow. This is useful for deliberate overrides but dangerous when names are generic.

name: Scope model
on: workflow_dispatch
permissions: {}
env:
  LOG_LEVEL: info
  APP_NAME: scope-lab
jobs:
  inspect:
    runs-on: ubuntu-24.04
    env:
      LOG_LEVEL: warning
    steps:
      - name: Job view
        run: printf 'app=%s level=%s\n' "$APP_NAME" "$LOG_LEVEL"
      - name: Step override
        env:
          LOG_LEVEL: debug
        run: printf 'step-level=%s\n' "$LOG_LEVEL"

The first step sees warning; the second sees debug. Neither change mutates the workflow file or GitHub repository configuration. This is runner process state created for each step.

5. Default environment variables are identity evidence, not editable configuration

GitHub sets default variables such as GITHUB_RUN_ID, GITHUB_RUN_ATTEMPT, GITHUB_SHA, GITHUB_REF, GITHUB_WORKFLOW, RUNNER_OS, and RUNNER_ARCH. They are not custom env entries and many have corresponding values in the github or runner contexts.

Current documentation states that default names beginning GITHUB_ and RUNNER_ cannot be overwritten. Treat that as a feature: run identity should not be casually shadowed by workflow-local configuration.

6. vars is centrally managed configuration, not a secret store

Configuration variables can live at organization, repository, or environment scope and are consumed through the vars context. They are appropriate for values such as REGION=lab-east, JAVA_DISTRIBUTION=temurin, or a non-sensitive feature mode used by many workflows.

If the same configuration-variable name exists at several levels, the lower/more specific level wins: repository over organization, environment over repository. There is an important lifecycle nuance: environment-level variables become available only after the job starts executing and do not retroactively overwrite values already evaluated in the env or vars contexts.

Variables render unmasked. If disclosure would matter, the value belongs in a secret or another protected credential system, not in vars.

7. Environment files are per-step communication channels

Each step receives paths such as GITHUB_ENV and GITHUB_OUTPUT. Writing text to those files is how a process tells the runner “make this environment value available to later steps” or “publish this named step output.” The file paths are runner-managed and should be treated as implementation channels, not persistent files.

steps:
  - name: Produce runtime data
    id: produce
    run: |
      echo 'COLOR=green' >> "$GITHUB_ENV"
      echo 'version=1.2.3-lab' >> "$GITHUB_OUTPUT"
      printf 'current step COLOR=%q\n' "${COLOR-}"  # not newly available here

  - name: Consume later
    env:
      VERSION: ${{ steps.produce.outputs.version }}
    run: |
      printf 'color=%s version=%s\n' "$COLOR" "$VERSION"

The producer step does not gain the newly written COLOR; the next step does. The output is separate and referenced through steps.produce.outputs.version.

8. Cross-job transfer requires an explicit job interface

Jobs normally execute on separate fresh hosted runners. A process environment or file created in one job is therefore not automatically available in another. For small control data, map a step output to a job output and consume it through needs.

jobs:
  produce:
    runs-on: ubuntu-24.04
    outputs:
      version: ${{ steps.meta.outputs.version }}
    steps:
      - id: meta
        run: echo 'version=1.2.3-lab' >> "$GITHUB_OUTPUT"

  consume:
    needs: produce
    runs-on: ubuntu-24.04
    steps:
      - env:
          VERSION: ${{ needs.produce.outputs.version }}
        run: printf 'received=%s\n' "$VERSION"

This interface is reviewable: the producer names the contract, the dependency graph shows the consumer, and the output value is tied to the producing job/run.

9. Outputs are control-plane data, not an artifact store

Current workflow syntax limits outputs to approximately 1 MB per job and 50 MB for all outputs in a workflow run, with sizing approximated using UTF-16. Even before those limits, forcing a binary or large report into an output is conceptually wrong: quoting, encoding, redaction, and log/debug handling become fragile.

Use a file and an artifact/file-transfer mechanism when the consumer needs a package, report, image, test result, or larger JSON document. Use an output for a version string, Boolean-like decision, path identifier, digest, small JSON control object, or other bounded interface value.

10. Production rule: keep a value ledger

For important pipelines, reviewers should be able to answer five questions for every nontrivial value: Where did it originate? Who may read it? How long should it live? How is it transported? What evidence proves the consumer got the intended value? This value ledger prevents ambient configuration from becoming hidden coupling.

Next lesson

Guided data-passing workflow

Build one controlled workflow that demonstrates workflow/job/step env, an optional repository configuration variable, GITHUB_ENV, GITHUB_OUTPUT, and a job output across needs.

Knowledge check

Why can a value written to GITHUB_ENV be read by the next step but not the producer step itself?

Why does GITHUB_ENV not solve cross-job data passing?

When should vars be preferred over workflow env?

A generated 120 MB binary needs another job. Should it be a job output?

Which wins when LOG_LEVEL is defined at workflow, job, and step scope?

Official references and version notes

Version and compatibility note

Version-sensitive variable/output behavior was rechecked against current primary GitHub documentation on 2026-09-09. Executable labs use a disposable repository, ubuntu-24.04, built-in Bash steps only, and explicit permissions: {}; no external action, secret, package, deployment, self-hosted runner, or cloud account is required. At verification time, workflow/job/step env uses the most specific scope; default GITHUB_*/RUNNER_* environment variables cannot be overwritten; the step that writes GITHUB_ENV does not see the new value but later steps in the same job do; GITHUB_ENV cannot set NODE_OPTIONS; GITHUB_OUTPUT is the current step-output channel; multiline environment/output values use delimiter syntax whose delimiter must not occur on a line by itself; and arbitrary data is safer as a file rather than delimiter-encoded text. Configuration variables are non-secret, have organization/repository/environment scopes, and are subject to current limits including 48 KB per variable, 500 repository variables, 1,000 organization variables, 100 environment variables, and a 256 KB combined repository+organization payload per workflow run. Job outputs are limited to 1 MB per job and 50 MB total per workflow run (size approximated using UTF-16), so large/binary evidence belongs in files/artifacts rather than outputs. GitHub Actions is continuously delivered, so regenerate only after rechecking these limits and commands. The chapter deliberately separates secrets from ordinary configuration; detailed secret handling begins later in the course.

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.