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:

A parameter in an Azure pipeline

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:

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:

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:

  1. 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
  2. Variable Scope Issues
    • Variables defined in one job are not automatically available in other jobs
    • Use isOutput=true and dependencies syntax for cross-job communication
    • Stage variables are not accessible from other stages
    • Environment variables set in scripts need task.setvariable command
  3. 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

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.

comments powered by Disqus