An Actions workflow is processed in stages. GitHub evaluates workflow expressions and schedules jobs, then a runner executes commands in its operating-system environment. Confusing those stages leads to conditions that cannot see a shell variable, outputs that remain strings and scripts that accidentally treat user input as executable syntax. This lesson follows one value from a manual input into an environment variable, then follows a step output through a job dependency. Its complete workflow uses no repository code or third-party actions, so the data flow stays visible.
Name the context for the decision
Contexts are named collections such as github, inputs, env, steps and needs. Each belongs to particular workflow locations and phases. The inputs context represents declared manual or reusable-workflow inputs; steps exposes prior step results within a job; needs exposes outputs of jobs that the current job explicitly depends on. Look up availability for the YAML key you are using rather than assuming every context exists everywhere.
Runner environment variables belong to the process environment when commands execute. Bash reads a variable with dollar syntax, while the workflow expression syntax uses dollar-brace delimiters with an expression inside. A variable written by a command cannot retroactively alter a job-level condition already evaluated before that command ran. Draw the intended path of the value: producer step, named output, job output, needs dependency and consumer. This makes missing connections easier to find than trial-and-error changes to delimiters.
Promote outputs across job boundaries
A step needs an id if later expressions should address its outputs. Writing a name=value line to GITHUB_OUTPUT creates a step output for subsequent workflow use. The job then declares an output mapping from that step output. A consuming job lists the producer under needs and reads the declared job output through the needs context. Each connection is explicit; sharing the same output name in two jobs does not connect them automatically.
Do not use outputs as a storage system for large reports or secrets. Keep them small, stable pieces of coordination data such as a selected mode, an artifact identifier or a validation result. A multiline value needs the documented delimiter format and a delimiter that cannot collide with the data. The example uses a single-line true value so the teaching focus remains on the connection. For a project report, use a file artifact and pass only the reference needed to retrieve it.
Handle strings and booleans intentionally
Step outputs are strings, even when their text is true, false or a number. A condition that treats a nonempty false string as a general truthy value can surprise you. fromJSON converts a valid JSON literal such as false into the corresponding boolean. Validate or control the producer's format before using that conversion; an arbitrary message is not valid JSON simply because it came from an output field.
Declared manual inputs can be typed, and the inputs context preserves supported types differently from raw event payload representations. Choose the context that matches your contract. Avoid complicated expressions that imitate a ternary operator with && and || when the desired middle value can be false or empty; the fallback may then win unexpectedly. Prefer a clearly named intermediate result or separate conditional steps. Readability matters because reviewers need to understand when a job gains access to credentials or performs an irreversible operation.
Keep untrusted values out of script construction
Pull request titles, branch names, issue text and manual string inputs are data controlled by users. Direct expression substitution inside a run script can insert that data into the script source before the shell parses it. Instead, pass the value through an environment variable and quote that variable when the command uses it. The complete workflow prints MESSAGE with a fixed printf format, so punctuation in the value is handled as data rather than new Bash syntax.
That boundary does not make every use safe automatically. A program can still interpret a value as a filename, option, regular expression or template. Validate the contract required by the consumer and use appropriate argument boundaries. Avoid dumping entire contexts into logs, because they can contain sensitive or irrelevant fields. Log the few nonsecret values needed to explain a decision. A well-designed workflow lets you trace the data without revealing credentials or trusting a user's text as code.
Sources and prerequisites
Review Actions basics, security and CLI usage. GitHub's contexts, expressions, job outputs and secure use references describe the contracts used here.
GitHub Actions Contexts, Expressions and Job Outputs Summary
- Know whether a value is evaluated by Actions or by a runner process.
- Promote step outputs to job outputs and declare needs dependencies.
- Control output types and keep user text outside shell source construction.
GitHub Actions Contexts, Expressions and Job Outputs Examples
Each example shows the code, the output it produces and what each part does.
Pass a step output to a dependent job
Save this complete file as .github/workflows/context-demo.yml. Run it manually with the default message to obtain the shown lines in the dependent job. The workflow output is a reasoned expected Actions result; the site build does not execute GitHub-hosted workflows.
The choose job promotes flag's output, report depends on choose and fromJSON interprets the controlled true string as a boolean. The message enters through env and is quoted in a fixed printf command. There is no direct substitution of message text into the run script's source.
name: Context demo
on:
workflow_dispatch:
inputs:
message:
description: Message to print as data
required: true
type: string
default: hello
permissions: {}
jobs:
choose:
runs-on: ubuntu-latest
outputs:
ready: ${{ steps.flag.outputs.ready }}
steps:
- id: flag
run: printf 'ready=true\n' >> "$GITHUB_OUTPUT"
report:
needs: choose
if: ${{ fromJSON(needs.choose.outputs.ready) }}
runs-on: ubuntu-latest
steps:
- name: Print selected message
env:
MESSAGE: ${{ inputs.message }}
run: |
printf 'ready job ran\n'
printf 'message: %s\n' "$MESSAGE"ready job ran
message: helloGitHub Actions Contexts, Expressions and Job Outputs in Real Projects
Parse a string flag into a real boolean
Run this standalone Node.js exercise to compare string presence with boolean meaning. It models the type distinction without claiming that JavaScript's rules emulate every Actions expression rule. Change the raw value to true and predict both lines before running again.
const raw = 'false';
console.log('nonempty string:', Boolean(raw));
const parsed = JSON.parse(raw);
if (typeof parsed !== 'boolean') throw new Error('Expected a boolean');
console.log('parsed boolean:', parsed);
const message = 'hello; this is still data';
console.log('message:', message);nonempty string: true
parsed boolean: false
message: hello; this is still dataThe string exists even though its textual value names false. Explicit parsing produces the boolean the consumer intended. In Actions, use the documented expression conversion and an intentional output contract rather than assuming that a producer's text is already typed.
GitHub Actions Contexts, Expressions and Job Outputs FAQ
Why can a job not read another job's step output directly?
A step output is scoped to its producing job. Declare a job output that maps to the named step's output, add the producing job to the consumer's needs list and read the job output through needs. Check every link in that path, including the step id and output name. A matching environment variable name or a similar job name does not establish the required dependency or output mapping.
Why is a false output sometimes treated as true?
A step output is a string, and a nonempty string can be truthy in conditions that test presence rather than its intended boolean meaning. Use a controlled output format and an explicit conversion such as fromJSON when the contract is a JSON boolean. Do not apply the conversion to arbitrary text without validation. Distinguish a missing output, an empty string and the literal false; they communicate different states.
Is an env variable enough to validate a pull request title?
It creates a safer boundary between expression data and shell source, but it does not establish that the title satisfies your program's semantic rules. Quote shell expansions and validate length, accepted format or downstream interpretation as needed. Avoid using the value as an executable command or template. Passing data safely and deciding whether that data is acceptable are related but separate responsibilities in the workflow.
Can I log the whole github context while debugging?
Prefer logging a small list of nonsecret fields needed for the current diagnosis. Whole contexts can contain sensitive token-related fields and large amounts of user-controlled data. Secret masking is not a reason to print everything indiscriminately. Identify the event name, relevant reference and selected nonsecret decision values, then inspect the workflow's documented context availability. This keeps diagnostic output useful without turning it into an accidental data dump.
GitHub Actions Contexts, Expressions and Job Outputs Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which conversion gives the practice program's raw false text boolean meaning?
GitHub Actions Contexts, Expressions and Job Outputs Quiz
2 questions to check what you learned.
What connects report to choose's declared output in the workflow?
- Giving both jobs the same runner
- needs: choose and the needs context
- Printing ready in a log
How should a user-controlled message enter the Bash printing step?
- By constructing the shell source from its text
- By disabling every quote in the command
- Through an environment variable used in a quoted fixed command