Skip to content

Passing Data Between Steps


You saw a preview of this with the steps context — now it’s time to build it properly. Steps in a job run in the same environment, one after another, but they don’t automatically share data the way lines in a single script would. If step two needs a value step one computed, you have to explicitly pass it along using step outputs, written through the special $GITHUB_OUTPUT file.

The Problem This Solves

Imagine a step that determines a version number, and a later step that needs to use it in a Docker tag. Without an explicit mechanism, there’s no way for the second step to know what the first one calculated — each run: block is its own shell invocation, and shell variables don’t persist across them.

steps:
  - run: |
      VERSION="1.4.2"
      echo "Version is $VERSION"

  - run: echo "Building version $VERSION"   # VERSION is empty here!

Try this yourself to see the failure mode:

mkdir gha-outputs-demo && cd gha-outputs-demo
git init
mkdir -p .github/workflows
cat > .github/workflows/broken-vars.yml << 'EOF'
name: Broken Variable Passing
on: workflow_dispatch

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - run: |
          VERSION="1.4.2"
          echo "Version is $VERSION"
      - run: echo "Building version $VERSION"
EOF
git add .
git commit -m "Add broken variable passing demo"
git branch -M main

Push and trigger it — the second step prints Building version with nothing after it. VERSION existed only inside the shell process of the first step, which terminates the moment that step finishes.

Fixing It With $GITHUB_OUTPUT

Every step can write key=value lines to a file path stored in the $GITHUB_OUTPUT environment variable. Anything written there becomes available afterward through steps.<step-id>.outputs.<key>.

steps:
  - id: version
    run: echo "value=1.4.2" >> "$GITHUB_OUTPUT"

  - run: echo "Building version ${{ steps.version.outputs.value }}"

Add the fixed version:

cat > .github/workflows/step-outputs.yml << 'EOF'
name: Step Outputs Demo
on: workflow_dispatch

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - id: version
        run: echo "value=1.4.2" >> "$GITHUB_OUTPUT"

      - run: echo "Building version ${{ steps.version.outputs.value }}"
EOF
git add .
git commit -m "Add working step outputs demo"

Push and trigger it — this time the second step correctly prints Building version 1.4.2. The id: version is required; without it, there’s no name to reference in steps.<id>.outputs.

Note

$GITHUB_OUTPUT is a file, not a shell variable — the >> appends a line to it. Using > instead of >> would overwrite anything a previous line in the same step already wrote, so always append.

Computing A Real Value

A slightly more realistic example: deriving a short git commit SHA and using it downstream.

name: Real Output Demo
on: workflow_dispatch

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - id: sha
        run: echo "short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"

      - run: echo "This build corresponds to commit ${{ steps.sha.outputs.short }}"
cat > .github/workflows/real-output.yml << 'EOF'
name: Real Output Demo
on: workflow_dispatch

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - id: sha
        run: echo "short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"

      - run: echo "This build corresponds to commit ${{ steps.sha.outputs.short }}"
EOF
git add .
git commit -m "Add real-world output demo using git SHA"
git push -u origin main

This is the first demo in this chapter that needs to be pushed to an actual GitHub repository to run (actions/checkout@v4 needs a real repo to check out), so create one on GitHub first and set it as your origin before pushing.

Multi-Line Output Values

A single echo key=value line breaks if the value itself contains newlines — the $GITHUB_OUTPUT format has a special delimiter syntax for that case:

- id: changelog
  run: |
    {
      echo "notes<<EOF"
      echo "- Fixed the login bug"
      echo "- Improved startup time"
      echo "EOF"
    } >> "$GITHUB_OUTPUT"

EOF here is just a delimiter string, not a special keyword — GitHub reads everything between the two EOF markers as the value of notes. Pick a delimiter unlikely to appear in your actual content if EOF itself might legitimately show up in the value.

Outputs Are Per-Job, Not Global

Step outputs written in one job are not visible in a different job, even through the same steps.<id>.outputs.<key> syntax — that syntax only resolves within the job where the step actually ran. Getting a value from one job into another requires job-level outputs, which work differently and get their own chapter next.

How Real DevOps Engineers Use This

Step outputs are the connective tissue of almost every nontrivial pipeline: a step that determines whether any relevant files changed feeding a conditional on a later step, a step that builds a Docker image tag from a git SHA or version file feeding the actual docker image build and docker image push commands, or a step that checks whether a deployment target is healthy feeding a decision about whether to proceed or roll back. The pattern is always the same — compute once, write it to $GITHUB_OUTPUT with a clear key name, then reference it by steps.<id>.outputs.<key> anywhere later in the same job. Marketplace actions (the ones installed with uses:) follow this exact same mechanism internally — when you see documentation for an action listing its “outputs,” that’s exactly what you’re reading from with this same steps.<id>.outputs.<key> syntax, whether the step ran a uses: action or your own run: command.

Clean up:

cd .. && rm -rf gha-outputs-demo

Tip

set -e doesn’t help you here if a step’s output-writing command silently fails to produce the expected format — always test a new $GITHUB_OUTPUT pattern with a simple echo in the very next step before building real logic on top of it.

Last updated on