How do you use conditions, dependencies, and when expressions to control pipeline flow?
Quick Answer
Azure DevOps YAML pipelines use condition expressions to control whether stages, jobs, or steps execute. Dependencies (dependsOn) define execution order, while conditions evaluate runtime expressions like branch names, variable values, or previous stage results to skip or include pipeline elements dynamically without modifying the YAML file.
Detailed Answer
Think of pipeline conditions like a programmable traffic light system on a highway. The traffic lights do not simply turn green in sequence — they evaluate real-time conditions: Is it rush hour? Is there an accident ahead? Is this lane for HOV vehicles only? Based on these conditions, different lanes open or close dynamically. Pipeline conditions work the same way: based on which branch triggered the build, whether previous stages succeeded, or what runtime parameters were set, different parts of the pipeline activate or skip without requiring separate pipeline files for each scenario.
The dependsOn property controls the execution graph. By default, stages execute sequentially in the order they are defined, and jobs within a stage run in parallel. DependsOn overrides these defaults: you can make stages run in parallel by giving them no dependencies, create diamond-shaped dependency graphs where two parallel stages converge into a final stage, or make jobs within a stage run sequentially. When dependsOn is set to an empty array, the stage or job has no prerequisites and starts immediately. When set to multiple stages, all listed stages must complete before the dependent stage begins.
Condition expressions evaluate at runtime using a rich expression language. The most common conditions check the build source branch (eq(variables['Build.SourceBranch'], 'refs/heads/main')), the result of previous stages (succeeded(), failed(), canceled()), pipeline variables, or parameters. Conditions use functions like eq(), ne(), contains(), startsWith(), and(), or(), and not(). The default condition for any stage, job, or step is succeeded(), meaning it only runs if all dependencies completed successfully. You override this with custom conditions to implement patterns like always-run cleanup, failure-only notifications, or branch-specific deployments.
Runtime parameters add another dimension of control. Unlike variables which are always strings, parameters support typed inputs including boolean, string, number, and object. Parameters are resolved at pipeline compile time (for template expressions ${{ }}) or at runtime (for macro expressions $()). A common pattern is a boolean parameter like deployToProduction that defaults to false, with a condition on the production stage that checks this parameter. Manual pipeline runs can override the parameter to true, enabling on-demand production deployments while automated triggers only build and test.
The expression syntax uses three different evaluation contexts. Template expressions (${{ }}) evaluate at pipeline compile time and can conditionally include or exclude entire stages, jobs, or steps from the YAML. Macro expressions ($()) evaluate at runtime when the step executes. Runtime expressions ($[ ]) evaluate when the stage or job starts. This distinction matters: template expressions can control pipeline structure (removing a stage entirely), while runtime expressions can only control whether an existing stage executes. Using the wrong expression type leads to confusing behavior where conditions appear to be ignored.
The production gotcha is the interaction between conditions and failure handling. The default condition succeeded() means that if Stage A fails, Stage B (which depends on A) is skipped entirely — including any cleanup or notification steps. To ensure a notification stage always runs regardless of previous failures, use condition: always(). But be careful: always() runs even when the pipeline is canceled, which may not be desired. The more precise condition: succeededOrFailed() runs on both success and failure but skips on cancellation. Another common mistake is using conditions on steps to skip deployment but forgetting that the deployment job still allocates an agent, consuming parallel job capacity even when all its steps are skipped.