Using Conditional Execution and Approval Gates in Azure DevOps YAML Pipelines
Learn how to combine branch-based conditional execution with environment approval gates in Azure DevOps YAML pipelines to automate safe deployments without losing governance.
02 Aug 2026, 22:24 UTC

Most delivery teams start with a simple rule: the main branch builds, everything else is a pull-request check. Then release day arrives and someone asks, "Can we skip the integration tests for hotfixes?" or "Who approved this production deploy?" The answer usually lives in a wiki page, a Slack thread, or a Classic UI pipeline that nobody wants to touch. The drift between what the pipeline actually does and what the team agreed to is where incidents breed.
A better pattern is to encode both the routing logic and the governance directly in the repository using a YAML multi-stage pipeline. The routing logic comes from conditional execution—stage-level condition settings that decide which jobs run based on branch, variable, or upstream status. The governance comes from environment approval gates, which pause a deployment until a designated reviewer signs off. Together they let you automate the happy path while keeping a human checkpoint on the targets that matter.
Why the repository is the right place for pipeline logic
Classic UI pipelines store their definition in Azure DevOps metadata, not in Git. That means changes are hard to review, impossible to branch, and easy to overwrite. YAML pipelines move the definition into the repo alongside the application code, so a pull request shows exactly what changed in the build and deploy process. For teams already practicing peer review on application code, extending that review to the pipeline itself is a small step with a large payoff.
Conditional execution as the first filter
Azure DevOps evaluates stage and job conditions using a small expression language. The most common pattern is comparing Build.SourceBranch against expected refs. For example, eq(variables['Build.SourceBranch'], 'refs/heads/main') returns true only for the main branch. You can combine expressions with and(), or(), and not() to build precise rules.
A subtle but important detail: conditions mix queue-time and runtime evaluation. Macro syntax like $(VarName) expands before the stage runs, while the variables['Name'] syntax resolves during execution. If you reference a variable set by a previous stage, use the runtime syntax inside a condition; macro syntax can expand too early and produce unexpected results. Because complex expressions that combine multiple variables can behave in ways that are hard to predict from reading alone, the safest verification is to queue a run on a non-default branch and confirm the skipped or executed status in the run summary.
Environment approval gates as the second filter
Conditional execution decides whether a stage runs. Environment approvals decide when it may proceed. In Azure DevOps, an environment is a logical grouping of deployment targets—think Staging, Production, or a specific Kubernetes namespace. You can add approval checks to an environment so that any pipeline stage targeting it pauses until the required reviewers approve.
The approval configuration lives in the Azure DevOps web portal under the environment settings, not in the YAML file. The YAML simply references the environment by name. This separation is deliberate: the pipeline author controls the flow, while the environment owner controls the access policy. It also means you can tighten or loosen approvals without editing the pipeline.
A worked example
Consider a repository that builds on every push, runs integration tests only for main and release branches, and deploys to Staging automatically but requires approval for Production. The pipeline below encodes that policy.
trigger:
branches:
include:
- main
- release/*
stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- script: echo "Building application"
- stage: IntegrationTests
condition: >-
and(succeeded(),
or(eq(variables['Build.SourceBranch'], 'refs/heads/main'),
startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')))
jobs:
- job: TestJob
steps:
- script: echo "Running integration tests"
- stage: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployStagingJob
environment: Staging
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying to staging"
- stage: DeployProduction
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployProductionJob
environment: Production
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying to production"
Run this on a feature branch and the Build stage executes while the remaining stages are skipped. Run it on main and all four stages become eligible; DeployStaging proceeds automatically, but DeployProduction waits for an approver if the Production environment has an approval check configured. To verify the skip behavior, push a commit to a non-default branch and confirm in the pipeline run summary that IntegrationTests, DeployStaging, and DeployProduction show as skipped.
Trade-offs and limitations
The main cost is latency. Environment approvals introduce a human wait into an otherwise automated pipeline. That is the point, but it means hotfixes need a documented path—either a break-glass procedure or a separate pipeline with its own approval policy.
Conditional expressions also grow brittle as they multiply. A stage condition that checks three variables and two branch prefixes is harder to reason about than two simpler stages. When conditions become complex, prefer splitting the logic into separate stages or jobs rather than nesting more functions. Finally, remember that condition replaces the default succeeded() dependency only when you override it; if you write a custom condition, include succeeded() explicitly or the stage may run even after an upstream failure.
Where to start
Start with one protected environment and one branch condition. Add an approval check to a Staging or Production environment in the Azure DevOps portal, reference that environment in a deployment job, and gate the stage with a single eq() condition on Build.SourceBranch. Queue a run on main to confirm the approval prompt appears, then push to a feature branch to confirm the stage is skipped. Once that loop feels natural, extend the pattern to additional environments and branch filters.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.