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.
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
| Function | Behavior 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 notationvariables.Build.SourceBranch. Do not wrap runtime variables in macro syntax$(Build.SourceBranch)inside thecondition: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
.exeor.dllavailable 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()withsucceededOrFailed(). If an engineer or pipeline timeout cancels a run,always()will still execute, whereassucceededOrFailed()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, usesucceededOrFailed(). - 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.
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 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?