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.
Learning objectives
- Classify values by origin, sensitivity, scope, lifetime, and transport mechanism before placing them in a workflow.
-
Explain workflow/job/step
envprecedence and distinguish theenvcontext from runner environment variables. -
Explain how
varsdiffers fromenvand why configuration variables are not a secret store. -
Use
GITHUB_ENVandGITHUB_OUTPUTas 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.
2. Mental model: configuration becomes process state, then explicit outputs
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.
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.
Knowledge check
Why can a value written to GITHUB_ENV be read by
the next step but not the producer step itself?
The runner processes the environment file after the step finishes and injects the value into subsequent steps in the same job.
Why does GITHUB_ENV not solve cross-job data
passing?
Jobs have separate execution environments. The environment-file update belongs only to later steps in the same job.
When should vars be preferred over workflow
env?
When the value is non-sensitive configuration that should be managed and reused across workflows or repository/environment scope rather than embedded in one workflow file.
A generated 120 MB binary needs another job. Should it be a job output?
No. Job outputs are small control data and have strict size limits; use file/artifact transport.
Which wins when LOG_LEVEL is defined at workflow,
job, and step scope?
The step-level value during that step; then job-level outside that step, then workflow-level outside the job override.
Official references and version notes
-
Variables
— current distinction between workflow
env, configuration variables, default variables, and secrets. - Variables reference — current default variables, naming rules, precedence, configuration-variable limits, and context guidance.
- Store information in variables — current examples for workflow/job/step environment variables, configuration variables, contexts, and cross-step/job data flow.
-
Workflow commands for GitHub Actions
— current environment files including
GITHUB_ENV,GITHUB_OUTPUT, multiline syntax, and restrictions. -
Passing information between jobs
— current job-output mapping and
needs.<job>.outputsconsumption. -
Workflow syntax for GitHub Actions
— current
env, job outputs,needs, output size limits, and workflow syntax. - Store and share data with workflow artifacts — current artifact model for files that must outlive a step/job filesystem or are inappropriate for small outputs.
-
GitHub Actions changelog: set-output update
— why new workflow code should use environment files rather than
the deprecated stdout
set-outputcommand.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.