By Jacob Strieb.
Published on May 28, 2025.
GitHub Actions workflows are separated into steps. Each step may “output” strings that can be used in expressions elsewhere in the workflow. By default, it is not possible to have arbitrary multiline string outputs.1
We can achieve reliable multiline outputs by serializing output strings to JSON in the workflow step, and deserializing them in the expression that uses them. Jump to the end for an example of the solution, or continue on for an explanation.
An Actions workflow step sets an output by appending strings of the form name=value
to the file whose path is stored in the environment variable GITHUB_OUTPUT
. That file is newline-delimited. In practice, setting and using outputs looks like this:
on:
push:
jobs:
job-name:
runs-on: ubuntu-latest
steps:
- id: make-output
run: |
echo "out_name=out_value" >> "${GITHUB_OUTPUT}"
echo "other_output=value2" >> "${GITHUB_OUTPUT}"
- run: |
echo "${{ steps.make-output.outputs.out_name }}"
echo "${{ steps.make-output.outputs.other_output }}"
Since the GITHUB_OUTPUT
file is newline-delimited, multiline outputs like the following do not work correctly. In the best case, the workflow terminates early with an error. In the worst case, the truncated output is used, and there is a hard-to-debug issue far downstream from the source of the problem.2
on:
push:
jobs:
error-job:
runs-on: ubuntu-latest
steps:
- id: make-output
run: |
# Causes an error
echo "multiline=$(
printf '%s\n%s\n%s\n' one two three
)" >> "${GITHUB_OUTPUT}"
- run: |
echo "${{ steps.make-output.outputs.multiline }}"
Instead of trying to output a multiline string literal, we can instead output a single-line, serialized JSON string by piping to jq
.
on:
push:
jobs:
job:
runs-on: ubuntu-latest
steps:
- id: make-output
run: |
# Serialize using jq; no error
echo "multiline=$(
printf '%s\n%s\n%s\n' one two three \
| jq --raw-input --compact-output --slurp
)" >> "${GITHUB_OUTPUT}"
- run: |
# Deserialize using jq
jq --raw-output <<"EOF"
${{ steps.make-output.outputs.multiline }}
EOF
--raw-input
flag instructs jq
to turn the raw string into a JSON serialized string--compact-output
flag ensures jq
outputs only a single line--slurp
flag ensures that jq
reads the entire input as one string instead of one string per lineDespite being illustrative, the previous example is a bit silly. If we wanted to transfer multiline strings between steps, we would just write them to a file, since the filesystem is shared across steps within the same job.
On the other hand, multiline outputs are actually required when the output of a job step must be used as input for another job step in the Actions workflow YAML, via an expression. For example, the actions/cache
workflow takes a path
input that is a newline-delimited list of globbing patterns and paths to cache. If we want the list of paths to be set by a previous job step, then the step must be able to output multiline strings.
To deserialize the multiline string in an Actions workflow expression, we use the fromJSON
function.
on:
push:
jobs:
job:
runs-on: ubuntu-latest
steps:
- id: make-output
run: |
# In real life this would determine which paths to cache, rather than
# being static
echo "multiline=$(
printf '%s\n' file1.txt file2.txt file3.txt \
| jq --raw-input --compact-output --slurp
)" >> "${GITHUB_OUTPUT}"
- id: cache
uses: actions/cache@v4
with:
key: key4
path: |
${{ fromJSON(steps.make-output.outputs.multiline) }}
- run: |
if [ '${{ steps.cache.outputs.cache-hit }}' = 'true' ]; then
cat file1.txt file2.txt file3.txt
else
date | tee file1.txt
sleep 1
date | tee file2.txt
sleep 1
date | tee file3.txt
fi
Technically, it is possible to output multiline strings using (officially sanctioned) nasty hacks such as writing a heredoc to the GITHUB_OUTPUT
file, or doing sketchy string replacement. Both of these will be buggy in rare cases, such as when user input contains either the heredoc delimiter or the string literals used for escaping.↩︎
There will be an error and early termination unless the input being stored contains only name=value
pairs separated by newlines.↩︎