Cross-Job Communication: Mastering dependencies and Output Variables

Apr 26, 2026 min read

You’ve successfully calculated a version number in Job A. Now you need to use that number to tag an image in Job B. You try to access the variable, but it comes up empty. You try $(JobA.myVar), then $[ dependencies.JobA.outputs['task.myVar'] ]. Still nothing. Frustration sets in. Why is it so hard to move a simple string between two parts of the same pipeline?

Azure DevOps YAML pipelines are highly decoupled by design. Every job runs on a fresh agent with no shared memory or disk. Moving data between jobs (or across stages) requires “Mastering the Dependencies”—a specific process of declaring an output variable, creating a dependency link, and then mapping the value using the correct runtime context. Most developers fail because they use macro syntax ($( )) instead of runtime expressions ($[ ]), or they forget the isOutput=true flag. This guide is the definitive manual for cross-job and cross-stage data flow, ensuring your pipelines communicate with 100% reliability.

1. The “Produce and Consume” Model

Data handover is a two-step process: you must first “Export” the data from the producing job and then “Ingest” it into the consuming job.

1.1: Step 1: Producing the Output

To make a variable available outside its current job, you must use the isOutput=true flag in a logging command. Without this flag, the variable is local to the current agent and will be destroyed when the job finishes. echo "##vso[task.setvariable variable=myVar;isOutput=true]myValue"

The Output Variable Protocol

P1R.ODRUuCnERScJrOiBptL2O.GG#iI#sNvOGsuotC[pOtuMatMs=AktN.rDsueetvariable]3.OSuRtnCodHreEerSTi`RnTAaT`sOdkReNpaemned`encies`

Crucial Rule: The task producing the variable must have a name property. This name becomes the reference key for all downstream lookups. You cannot use the task’s display name.

Security Warning: Output variables are not automatically masked in logs. If you need to pass a secret, you must explicitly use the issecret=true flag: ##vso[task.setvariable ...;isOutput=true;issecret=true].

1.2: Step 2: Declaring the Dependency

A job can only “see” variables from jobs it explicitly depends on. Use the dependsOn property to create the logical link. In a multi-stage pipeline, if you need a variable from three jobs ago, you must ensure a continuous chain of dependsOn links exists from the producer to the consumer.

2. Consuming Data Across Jobs (Same Stage)

When jobs are in the same stage, you use the dependencies context.

2.1: The Mapping Pattern

The most reliable way to ingest an output is to map it to a local variable in the receiving job’s variables: block. This creates a clean “API contract” for your job logic and avoids using long, complex strings deep inside your task inputs.

jobs:
- job: Producer
  steps:
  - script: echo "##vso[task.setvariable variable=token;isOutput=true]abc123"
    name: GetAuth

- job: Consumer
  dependsOn: Producer
  variables:
    # Syntax: $[ dependencies.JobName.outputs['TaskName.VariableName'] ]
    secretToken: $[ dependencies.Producer.outputs['GetAuth.token'] ]
  steps:
  - script: ./deploy.sh --token $(secretToken)

2.2: Handling Multiple Dependencies

You can aggregate data from multiple parallel builds by mapping each one individually. A common pattern is a “Collector” job that waits for three regional builds to finish and then passes all three artifact versions to a single deployment task.

3. Consuming Data Across Stages

Moving data across stages requires an extra level of addressing using the stageDependencies context.

3.1: The stageDependencies Context

The syntax expands to include the source stage name: $[ stageDependencies.StageName.JobName.outputs['TaskName.VariableName'] ].

stageDependencies 3-Tier Addressing

[1.STJ-AoGbTE:a:sCkBo:UmIpVLieDlres]ion[STAGE:TEST][23..STJ-[AoGbVvSE:a:t:raPi$gDua[eEsbs.PhltJLeaoOsgbY:e.DT]eaps.k..]]

If the producer is a Deployment Job, the syntax requires the lifecycle hook name (usually Deploy) to be included twice: $[ stageDependencies.Prod.DeployJob.outputs['Deploy.StepName.VarName'] ].

3.2: Short-circuiting the Syntax

For complex pipelines, the stageDependencies string can become unreadable. You can simplify this by mapping the outputs into stage-level variables. This creates a “Global Manifest” where the first stage produces a JSON object, and all subsequent stages ingest it using a shorter, local reference.

4. Communication in Dynamic Matrices

Matrix jobs present a unique challenge: they produce multiple versions of the same variable name.

4.1: The Matrix Key Problem

When Job A is a matrix, Azure DevOps appends the matrix key to the job name in the dependency object. To access a specific instance, you must use the key: dependencies.JobA.outputs['Linux_Leg.TaskName.VariableName'].

4.2: Aggregating Matrix Results

If you need to combine outputs from 50 matrix legs into one manifest, do not attempt to map 50 variables. Instead, have each matrix leg upload its result as a small Pipeline Artifact. Your collector job then downloads all artifacts and uses a script to merge them into a single file. This avoids the 1MB limit for output variables and handles dynamic counts gracefully.

5. Troubleshooting Empty Variables

If your variable is coming up as null or empty, follow this checklist:

  1. Check the Producer Status: Did the producing task actually run? If a task is skipped due to a condition, its outputs are never created.
  2. Verify the Name: Is the name: of the producing task exactly matching the key in your $[ ] expression?
  3. Timing Check: Are you trying to access the variable at compile-time (${{ }})? Cross-job variables are runtime only.
  4. The JSON Dump: Add a script to your consuming job to echo "##[debug]$(dependencies)". This will print the entire available context to the logs, allowing you to see exactly which keys and values the orchestrator has resolved.

Hands-On Example: The “Artifact Handover”

Consider a pipeline where Stage 1 calculates a VulnerabilityScore. Stage 2 (Deploy) only runs if the score is less than 10.

The Artifact Handover Pattern

[1.SCSAcNorJeO:B5]2[.AC(PoCPnhRdeOicVtkAiLosntG:aAgTseEcDoe]rp[es)P<AS1S0ED]3.Ut[saegDEsrPceLolOreYeasJteOoB]
- stage: Build
  jobs:
  - job: Scan
    steps:
    - script: |
        score=5 # Logic to calculate
        echo "##vso[task.setvariable variable=score;isOutput=true]$score"
      name: SecurityCheck

- stage: Deploy
  dependsOn: Build
  condition: lt(stageDependencies.Build.Scan.outputs['SecurityCheck.score'], 10)
  jobs:
  - job: Push
    steps:
    - script: ./deploy.sh

Key Takeaways

  1. Always use isOutput=true for data that needs to leave the agent.
  2. Declare dependsOn to open the communication channel.
  3. Use $[ ] for all cross-job mapping; never use $( ) for these values.
  4. Namespace your variables: Stage -> Job -> Task -> Variable. This hierarchy ensures your data flow is predictable and easy to debug.

Sources