Pass Outputs Between GitHub Actions Jobs
Pass data between GitHub Actions jobs with job-level outputs, $GITHUB_OUTPUT, needs, multiline values and fromJSON, plus the pitfalls that leave outputs empty.
At a glance
- Last reviewed
- Reading time
- 3 min read
Code samples are not run in a live repository. See our editorial standards.
On this page
Define the value in jobs.<job_id>.outputs, map it to a step output written to $GITHUB_OUTPUT, and read it in a downstream job through needs.<job_id>.outputs.<name>. The downstream job must list the producer in needs. For a related walkthrough, see Fix ‘Resource not accessible by integration’ (403) in GitHub Actions.
Minimal working example
name: pass-outputs
on: push
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.set_version.outputs.version }}
steps:
- id: set_version
run: echo "version=1.0" >> "$GITHUB_OUTPUT"
test:
runs-on: ubuntu-latest
needs: build
steps:
- run: echo "Build version is ${{ needs.build.outputs.version }}"
The outputs block on build maps the job output version to the step output steps.set_version.outputs.version. Jobs run in separate runners, so a step output is invisible to other jobs unless you expose it this way.
To verify, the test job log should contain Build version is 1.0. If the value is empty, check that the step id, the output name and the needs entry match exactly.
Job outputs suit small strings. For files or large data, use actions/upload-artifact and actions/download-artifact instead.
Write step outputs with $GITHUB_OUTPUT
Append name=value to the file at $GITHUB_OUTPUT. The step needs an id, or you cannot reference its outputs.
- name: Set result
id: result-step
run: echo "result=success" >> "$GITHUB_OUTPUT"
- name: Use result
run: echo "Result was $RESULT"
env:
RESULT: ${{ steps.result-step.outputs.result }}
Passing the value through env instead of interpolating ${{ }} directly into the script is safer. If the value comes from untrusted input such as a pull request title, direct interpolation allows script injection.
On Windows runners using PowerShell, write to the same file:
"result=success" >> $env:GITHUB_OUTPUT
The old ::set-output workflow command is deprecated. Use $GITHUB_OUTPUT in all new workflows.
Consume outputs in another job
name: consume-outputs
on: push
jobs:
producer:
runs-on: ubuntu-latest
outputs:
build_id: ${{ steps.set_output.outputs.id }}
steps:
- id: set_output
run: echo "id=123456" >> "$GITHUB_OUTPUT"
consumer:
runs-on: ubuntu-latest
needs: producer
steps:
- env:
BUILD_ID: ${{ needs.producer.outputs.build_id }}
run: 'echo "Received build ID: $BUILD_ID"'
The consumer log should show Received build ID: 123456. If it is empty, confirm that jobs.producer.outputs.build_id points at the right step ID and output name (id here).
The needs context only contains jobs listed in that job’s own needs. A job that depends on consumer cannot read producer outputs unless it also lists producer.
You can also check the upstream outcome with needs.producer.result, which is success, failure, cancelled or skipped. For example, use if: needs.producer.result == 'success' on a job or step.
Handle multiline outputs
Use a delimiter so the runner treats several lines as one value.
jobs:
job1:
runs-on: ubuntu-latest
outputs:
multiline: ${{ steps.set-output.outputs.multiline }}
steps:
- id: set-output
run: |
{
echo 'multiline<<EOF_7f3a91'
echo 'Line 1: Hello'
echo 'Line 2: World'
echo 'Line 3: 42'
echo 'EOF_7f3a91'
} >> "$GITHUB_OUTPUT"
job2:
runs-on: ubuntu-latest
needs: job1
steps:
- name: Read multiline output
env:
MULTILINE: ${{ needs.job1.outputs.multiline }}
run: |
while IFS= read -r line; do
echo "Parsed line: $line"
done <<< "$MULTILINE"
The syntax is name<<DELIMITER, then the value lines, then the delimiter alone on a line. Without it, the first newline ends the value. Pick a delimiter that cannot appear in the payload. For arbitrary content, such as command output, generate a random one, for example with openssl rand -hex 8.
The job2 log should show Parsed line: Line 1: Hello, then the other two lines. If only the first line arrives, the delimiter is missing or collides with the content.
Work with JSON outputs
Outputs are always strings. Use fromJSON to turn a JSON string into an object, for example to build a dynamic matrix.
name: dynamic-matrix
on: push
jobs:
job1:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
echo 'matrix={"include":[{"project":"foo","config":"Debug"},{"project":"bar","config":"Release"}]}' >> "$GITHUB_OUTPUT"
job2:
needs: job1
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.job1.outputs.matrix) }}
steps:
- run: echo "Project ${{ matrix.project }}, config ${{ matrix.config }}"
Single quotes around the echo argument avoid escaping the inner double quotes. The output must be a single line of valid JSON. If it is malformed, fromJSON fails and the job errors.
fromJSON also converts the string "true" to a Boolean, which is useful in if: conditions:
- name: Use flag
if: ${{ fromJSON(needs.job1.outputs.debug) }}
run: echo "Debug mode on"
Without fromJSON, the comparison is against the string, for example needs.job1.outputs.debug == 'true'.
Common mistakes
- Wrong step ID or output name.
steps.<id>.outputs.<name>resolves to an empty string when either is misspelled. Compare theid:and thename=written to$GITHUB_OUTPUT. - Missing
needs. Withoutneeds: job1,needs.job1is not available injob2, and the job may also run in parallel before the value exists. Putneedsat the same indentation level asruns-on. - Secrets in outputs. Do not pass secrets through job outputs. GitHub may skip outputs that contain secret values, and the downstream value arrives empty. Pass secrets with
secretsor a secret store in each job instead. - Writing outputs in the wrong place. Output values must be written to
$GITHUB_OUTPUTin a step. Setting an environment variable with$GITHUB_ENVmakes it available to later steps in the same job only, not to other jobs.
To verify a pair of outputs, the reference example from the docs maps output1 and output2 from step1 and step2. In job2, echo "$OUTPUT1 $OUTPUT2" should print hello world.
Spotted an error? Report it on our Contact page and see our editorial standards for how we correct articles.