A Guide to Compile-Time, Template, and Runtime Expressions in Azure Pipelines
Azure Pipelines support two types of expressions to control pipeline behavior: compile-time expressions (evaluated at creation) and runtime expressions (evaluated during execution).
Compile-time expressions are evaluated when the pipeline is created, making them ideal for build configuration and branch-specific logic. Template expressions fall under the same category.
Runtime expressions are evaluated during execution. Use them when you need to make decisions based on test results or build artifacts.
Before focusing on the two types of expression, let us start by examining parameters and variables, the two main ways to store and manage values and pass them to expressions in Azure Pipelines.
Pipeline parameters
Pipeline parameters are evaluated at compile time and cannot be updated during pipeline execution. Parameters are referenced using the ${{ parameters.name }} syntax. For example:
parameters:
- name: shouldBuildProject
type: boolean
default: true
This parameter appears as a checkbox in the Azure Pipelines UI:

Variables
Variables can be defined at different scopes, each with its own visibility and lifetime:
| Scope | Visibility | Use Case |
|---|---|---|
| Pipeline | Global (all stages, jobs, steps) | Configuration values used throughout the pipeline |
| Stage | Limited to specific stage and its jobs | Stage-specific settings |
| Job | Limited to specific job and its steps | Job-specific configurations |
All three scopes are shown in this example:
# Pipeline-level variables
variables:
# Inline variables
globalConfig: 'production'
# Variable groups
- group: Common.Variables
- group: Production.Variables
# Key Vault variables
- group: KeyVault.Secrets
stages:
- stage: Build
variables:
buildConfiguration: 'Release' # Stage-level variable
jobs:
- job: Compile
variables:
compilerFlags: '--optimize' # Job-level variable
steps:
- script: |
echo "Global config: $(globalConfig)"
echo "Build config: $(buildConfiguration)"
echo "Compiler flags: $(compilerFlags)"
Variables can be referenced using three different syntaxes, each with its specific use case:
# Macro syntax – most common, used in task inputs and scripts
$(variableName)
# Template expression syntax – used in templates and compile-time expressions
${{ variables.variableName }}
# Runtime expression syntax – used in conditions and runtime expressions
$[variables.variableName]
Important notes about syntax usage:
$(variableName)is the most common and works in most contexts${{ variables.variableName }}is used when you need the value at compile time$[variables.variableName]is specifically for runtime expressions in conditions
Unlike parameters, variables can be modified dynamically during pipeline execution.
In the following example, hasUnitTests is referenced using $(hasUnitTests), which means that the if-condition it is evaluated during pipeline execution:
steps:
- script: echo "Building the project in $(buildConfiguration) configuration"
displayName: "Build Configuration"
- script: |
if [ "$(hasUnitTests)" == "true" ]; then
echo "Running Unit Tests..."
else
echo "Skipping Unit Tests."
fi
displayName: "Check and Run Unit Tests"
Compile-time expressions
The syntax for compile-time expressions looks like ${{ <expression> }} and these expressions can be used with pipeline parameters or statically defined variables.
Compile-time expressions are evaluated during pipeline compilation (when the pipeline is getting created). This means these expressions cannot react to changes that occur during pipeline execution.
Example of a job with compile-time conditions:
jobs:
- job: Build
condition: ${{ eq(parameters.shouldBuildProject, true) }}
steps:
- script: echo "Building project..."
condition: ${{ and(eq(parameters.configuration, 'Release'), eq(parameters.enableTests, true)) }}
- job: Test
dependsOn: Build
condition: ${{ parameters.runTests }} # Simple parameter reference
steps:
- script: echo "Running tests..."
Template Expressions
Template expressions are a special type of compile-time expression used in YAML templates. They enable you to create reusable pipeline components with parameterized behavior. Templates are evaluated at compile time and can include conditional logic based on parameter values.
Here’s an example of a template:
# build-template.yml
parameters:
solution: ''
buildConfiguration: 'Release'
runTests: true
steps:
- script: dotnet build ${{ parameters.solution }} --configuration ${{ parameters.buildConfiguration }}
displayName: 'Build Solution'
# Conditional inclusion of steps based on parameter
- $:
- script: dotnet test
displayName: 'Run Tests'
And here’s how to use it in your pipeline:
# azure-pipelines.yml
trigger:
- main
jobs:
- job: Build
steps:
- template: build-template.yml
parameters:
solution: '**/*.sln'
buildConfiguration: 'Debug'
runTests: false
Runtime Expressions
Runtime expressions use either the macro syntax $(<expression>) or the runtime expression syntax $[<expression>]. These expressions:
- Can only use variables, not parameters
- Are evaluated during pipeline execution
- Support dynamic values that change during the run
- Can access task outputs and pipeline state
Here are examples of runtime expressions:
# Using array syntax (preferred for runtime conditions)
condition: eq(variables['hasUnitTests'], 'true')
# Using $[] syntax (alternative syntax)
condition: $[eq(variables['hasUnitTests'], 'true')]
# Complex condition with multiple checks
condition: $[and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), eq(variables['Build.Reason'], 'PullRequest')))]
Note: In conditions, the $[] wrapper is optional when using functions like eq(), and(), etc. However, when directly referencing variables in other contexts, you must use either $(name) or $[variables.name].
Example of runtime expression with system variables:
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
This expression is evaluated when the pipeline runs, allowing dynamic decision-making based on variable values set during execution.
Another example of dynamically setting a runtime variable:
- script: echo "##vso[task.setvariable variable=hasUnitTests]true"
The ##vso prefix is a special logging command syntax that Azure Pipelines uses to perform various operations during pipeline execution. These commands follow the format: ##vso[area.command]value
The isOutput property
When isOutput=true is specified in a task, the variable becomes accessible across dependent jobs or tasks. These variables are referenced using the dependencies.<TaskName>.outputVariables['<VariableName>'] syntax.
Example:
- script: echo "##vso[task.setvariable variable=hasUnitTests;isOutput=true]true"
name: MyTask
- script: echo "This is a dependent task"
condition: eq(dependencies.MyTask.outputVariables['hasUnitTests'], 'true')
Common Pitfalls and Best Practices
When working with expressions in Azure Pipelines, be aware of these common issues:
-
Mixing Expression Types
- Don’t use runtime variables (
$(var)) in compile-time expressions ($) - Parameters can only be used with compile-time expressions (
${{ parameters.name }}) - Template expressions cannot access runtime values
- Don’t use runtime variables (
- Variable Scope Issues
- Variables defined in one job are not automatically available in other jobs
- Use
isOutput=trueand dependencies syntax for cross-job communication - Stage variables are not accessible from other stages
- Environment variables set in scripts need
task.setvariablecommand
- Parameter vs. Variable Choice
- Use parameters for:
- Template customization
- Build configuration selection
- Feature flags that must be set before runtime
- Use variables for:
- Values that change during execution
- Task outputs that need to be shared
- Environment-specific settings
- Use parameters for:
Conclusion
The key takeaway: Use compile-time expressions with parameters for fixed values needed at pipeline creation, and runtime expressions with variables for dynamic behavior based on execution state.
Feel free to share this article or leave a comment.