7.2 Job Execution Order: DAG Dependencies, Parallelism & Conditions

Key Takeaways

  • Declaring dependsOn: [] on a stage removes its implicit dependency on the previous stage and makes it start immediately in parallel.
  • Fan-out runs several independent stages from one upstream stage; fan-in makes a downstream stage depend on all of them, which is how parallel quality checks converge before packaging.
  • The default condition on every stage and job is succeeded(), so adding any custom condition removes that implicit check unless you restate it.
  • Status functions succeeded(), failed(), always() and canceled() combine with and(), or() and eq() to express rules such as deploy only from main or a release branch.
  • Referencing variables in a condition requires the runtime syntax variables['Build.SourceBranch'] rather than a compile-time template expression.
Last updated: September 2026

7.2 Job Execution Order: DAG Dependencies, Parallelism & Conditions

Default ordering is rarely the fastest ordering. dependsOn converts the implicit sequence into an explicit directed acyclic graph, and condition decides whether a stage or job actually executes once its dependencies finish.

1. Dependency Management and DAG Topologies

By default, Azure Pipelines applies specific sequencing rules:

  • Stages: Run sequentially in the exact order they are listed in the YAML document.
  • Jobs within a Stage: Run concurrently in parallel by default, up to the maximum number of available parallel agent slots!

To control execution order, Azure Pipelines uses the dependsOn keyword. This allows you to construct sophisticated Directed Acyclic Graphs (DAGs).

Parallel vs. Sequential Stage Orchestration

To force stages to run sequentially, or to allow them to run concurrently, use dependsOn:

stages:
  # Stage 1: Compiles application
  - stage: Build
    displayName: 'Compile & Package'
    jobs:
      - job: BuildApp
        steps:
          - script: echo "Compiling..."

  # Stage 2: Security scanning - runs concurrently with Build!
  - stage: SecurityScan
    displayName: 'Static Code Analysis'
    dependsOn: [] # Empty list removes the implicit dependency on the previous stage
    jobs:
      - job: SonarAnalysis
        steps:
          - script: echo "Analyzing security vulnerabilities..."

  # Stage 3: Integration Tests - requires BOTH Build and SecurityScan to finish
  - stage: IntegrationTest
    displayName: 'Integration Testing'
    dependsOn:
      - Build
      - SecurityScan
    jobs:
      - job: RunTests
        steps:
          - script: echo "Executing API tests..."

Fan-Out and Fan-In Topology

In enterprise pipelines, a common requirement is fan-out (triggering multiple concurrent test suites or target deployments once a build succeeds) followed by fan-in (aggregating results before promoting to production).

                  ┌──► [Job: Unit Tests (Ubuntu)] ──────┐
                  │                                     │
[Job: Compile] ───┼──► [Job: Unit Tests (Windows)] ─────┼──► [Job: Package & Publish]
                  │                                     │
                  └──► [Job: Security & License Scan] ──┘
jobs:
  - job: Compile
    steps:
      - script: echo "Compiling source code..."

  - job: TestUbuntu
    dependsOn: Compile
    pool: { vmImage: 'ubuntu-latest' }
    steps:
      - script: echo "Testing on Linux..."

  - job: TestWindows
    dependsOn: Compile
    pool: { vmImage: 'windows-latest' }
    steps:
      - script: echo "Testing on Windows..."

  - job: SecurityScan
    dependsOn: Compile
    steps:
      - script: echo "Scanning dependencies..."

  # Fan-In aggregation job
  - job: Package
    dependsOn:
      - TestUbuntu
      - TestWindows
      - SecurityScan
    steps:
      - script: echo "Packaging final release artifacts..."

2. Conditions and Expressions

Every stage, job, and step evaluates a condition before starting. If the condition evaluates to false, the item is skipped.

Default Conditions

If you omit the condition: property:

  • For steps, jobs, and stages, the default condition is implicitly succeeded().
  • This means the step/job/stage will execute only if all previous items in its dependency graph completed with a status of Succeeded.

Built-in Status Check Functions

FunctionBehavior Description
succeeded()Returns true if all prior direct dependencies succeeded or partially succeeded. (Default behavior).
failed()Returns true if any prior direct dependency failed. Commonly used to trigger automated failure alerts or rollback tasks.
always()Returns true regardless of whether previous dependencies succeeded, failed, or partially succeeded, unless the pipeline was explicitly canceled by a user.
canceled()Returns true only if the pipeline run was explicitly canceled by a user or an upstream timeout. Used for urgent cancellation cleanup.
succeededOrFailed()Returns true if dependencies either succeeded or failed. Does NOT execute if the pipeline was canceled. Recommended for log uploaders and test result publishers.

Custom Conditional Expressions

You can combine status check functions with logical operators and variable checks:

  • Logical operators: and(), or(), not()
  • Comparison functions: eq(), ne(), lt(), gt(), startsWith(), endsWith(), contains(), in()
# Example 1: Deploy to Production only if upstream succeeded AND branch is main
- stage: DeployProd
  dependsOn: DeployQA
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))

# Example 2: Run cleanup step on failure or cancellation
- step:
  script: ./cleanup-temp-resources.sh
  displayName: 'Emergency Resource Teardown'
  condition: or(failed(), canceled())

# Example 3: Execute integration test only on scheduled runs or manual dispatches
- job: LongRunningPerfTests
  condition: in(variables['Build.Reason'], 'Schedule', 'Manual')

[!IMPORTANT] When authoring conditions that reference variables, use index syntax variables['Build.SourceBranch'] or dot notation variables.Build.SourceBranch. Do not wrap runtime variables in macro syntax $(Build.SourceBranch) inside the condition: expression, as macros are not expanded inside conditional expression trees.


3. Production-Grade Multi-Stage YAML Pipeline Walkthrough

The following complete pipeline demonstrates a production enterprise architecture: compiling code, running tests concurrently, publishing artifacts, deploying sequentially to Dev and QA, and deploying to Production with branch gating.

name: $(Date:yyyyMMdd)$(Rev:.r)

trigger:
  branches:
    include:
      - main
      - releases/*
  paths:
    exclude:
      - docs/**
      - README.md

variables:
  vmImageName: 'ubuntu-latest'
  buildConfiguration: 'Release'

stages:
  # ==========================================================================
  # STAGE 1: BUILD & TEST
  # ==========================================================================
  - stage: BuildStage
    displayName: 'Build & Unit Test'
    jobs:
      - job: CompileApp
        displayName: 'Compile .NET Microservice'
        pool:
          vmImage: $(vmImageName)
        steps:
          - checkout: self
            fetchDepth: 1
          - task: UseDotNet@2
            inputs:
              version: '8.x'
          - script: dotnet build --configuration $(buildConfiguration)
            displayName: 'Execute dotnet build'
          - script: dotnet test --configuration $(buildConfiguration) --logger trx
            displayName: 'Execute unit tests'
          - task: PublishTestResults@2
            condition: succeededOrFailed()
            inputs:
              testResultsFormat: 'VSTest'
              testResultsFiles: '**/*.trx'
          - task: DotNetCoreCLI@2
            displayName: 'Package Application'
            inputs:
              command: 'publish'
              publishWebProjects: true
              arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
          - publish: $(Build.ArtifactStagingDirectory)
            artifact: drop
            displayName: 'Publish Build Drop'

  # ==========================================================================
  # STAGE 2: DEPLOY TO DEVELOPMENT
  # ==========================================================================
  - stage: DeployDev
    displayName: 'Deploy to Development'
    dependsOn: BuildStage
    condition: succeeded()
    jobs:
      - deployment: DeployWebDev
        displayName: 'Deploy to Dev App Service'
        environment: 'development'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop
                - script: echo "Deploying package to Dev environment..."

  # ==========================================================================
  # STAGE 3: DEPLOY TO QA & INTEGRATION TESTS
  # ==========================================================================
  - stage: DeployQA
    displayName: 'Deploy to QA'
    dependsOn: DeployDev
    condition: succeeded()
    jobs:
      - deployment: DeployWebQA
        displayName: 'Deploy to QA App Service'
        environment: 'qa'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop
                - script: echo "Deploying package to QA environment..."

  # ==========================================================================
  # STAGE 4: DEPLOY TO PRODUCTION (RESTRICTED TO MAIN BRANCH)
  # ==========================================================================
  - stage: DeployProd
    displayName: 'Deploy to Production'
    dependsOn: DeployQA
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployWebProd
        displayName: 'Deploy to Production App Service'
        environment: 'production'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop
                - script: echo "Executing zero-downtime deployment to Production..."

4. Realistic Exam Scenarios & Common Traps

Scenario: The Parallel Quality Verification Bottleneck

Context: An enterprise team notices their multi-stage pipeline takes 45 minutes to execute. Analysis reveals that the pipeline runs Build, then StaticCodeAnalysis, then ContainerLinting, then DependencyVulnerabilityScan sequentially. Solution: Configure StaticCodeAnalysis, ContainerLinting, and DependencyVulnerabilityScan with dependsOn: [] (empty array) so they trigger simultaneously with Build right when the pipeline launches. The downstream Package stage then specifies dependsOn: [Build, StaticCodeAnalysis, ContainerLinting, DependencyVulnerabilityScan] to assemble the artifacts once all quality checks pass. This cuts total execution duration from 45 minutes to 14 minutes.

Common Exam Traps to Avoid

  • Trap: Expecting workspace files to persist across jobs. Candidates often assume that compiling an executable in Job 1 leaves the .exe or .dll available for Job 2. Azure Pipelines schedules jobs on independent agents! Files must be published as pipeline artifacts or container images to be consumed downstream.
  • Trap: Confusing always() with succeededOrFailed(). If an engineer or pipeline timeout cancels a run, always() will still execute, whereas succeededOrFailed() will not execute on user cancellation. If a task must run only when jobs naturally complete (win or lose) but abort if a developer hits Cancel, use succeededOrFailed().
  • Trap: Assuming jobs inside a stage run in sequence. Unless you explicitly define dependsOn: at the job level, all jobs within a single stage are dispatched concurrently in parallel by default, up to available pool concurrency.
Loading diagram...
Directed Acyclic Graph (DAG) Pipeline Execution Topology
Test Your Knowledge

An organization requires a multi-stage Azure Pipelines YAML definition where static security analysis, code quality linting, and third-party license auditing run concurrently at the start of the pipeline without waiting for the compilation stage. The packaging stage must only run after all four initial stages finish successfully. How should the pipeline dependencies be configured?

A
B
C
D
Test Your Knowledge

A release engineer needs to configure an Azure Pipelines YAML stage that deploys hotfixes to the Production environment. The stage must run only when all upstream verification stages succeed and when the triggering Git branch is either 'main' or begins with 'refs/heads/hotfix/'. Which condition expression correctly implements this requirement?

A
B
C
D