Copyright (c) 2026 MindMesh Academy. All rights reserved. This content is proprietary and may not be reproduced or distributed without permission.

2.3.2. Passing Data Between Steps and Jobs

💡 First Principle: There are exactly four data channels in a run — shell state (within one step), environment files (between steps), job outputs (between jobs, small values), and artifacts (between jobs, files) — and choosing the right one is entirely determined by how far the data must travel and how big it is.

Step to step, same job. Give the producing step an id: and write to $GITHUB_OUTPUT:

- id: meta
  run: echo "version=1.4.2" >> "$GITHUB_OUTPUT"
- run: echo "Version is ${{ steps.meta.outputs.version }}"

$GITHUB_ENV does the same job for environment variables; $GITHUB_PATH for PATH entries.

Job to job. Job outputs are declared at the job level, sourced from a step output, and read through needs:

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.meta.outputs.version }}
    steps:
      - id: meta
        run: echo "version=1.4.2" >> "$GITHUB_OUTPUT"
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh "${{ needs.build.outputs.version }}"

Two constraints: job outputs are strings (use toJSON/fromJSON for structures) and, critically, job outputs containing secrets are redacted and not passed. Anything sensitive must move through the secrets system, not through outputs.

Files between jobs always means artifacts — upload-artifact in the producer, download-artifact in the consumer, which is the only supported way to move a compiled binary from build to deploy. And reusable workflow outputs extend the same idea across the workflow_call boundary, as shown in 2.1.3.

Matrix jobs deserve one warning: because all variants share a job ID, their outputs collide — the last writer wins, non-deterministically. The standard pattern is per-variant artifacts (unique names, as in build-${{ matrix.os }}) and a downstream job that downloads them all with a pattern.

⚠️ Exam Trap: echo "X=1" >> $GITHUB_ENV followed by echo $X in the same step prints nothing — environment files are read between steps. Within a step, plain shell assignment is the answer. Expect at least one snippet built on this.

Reflection Question: A job output is a string, redacted if secret, and capped in size. Given those three constraints, what kind of data should never travel as a job output — and which channel takes each of those cases instead?

See how it connects
Alvin Varughese
Written byAlvin Varughese
Founder18 professional certifications