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 pipeline creation) and runtime expressions (evaluated during pipeline execution). Template expressions, a subcategory of compile-time, help you create reusable pipeline components.

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.

Want to dive deeper into all this terminology? Read along to learn all the details.

Table of Contents

  1. Pipeline Parameters - Static values set before pipeline runs
  2. Variables - Dynamic values that can change during execution
  3. Compile-time Expressions - For build configuration and static logic
  4. Template Expressions - For reusable pipeline components
  5. Runtime Expressions - For dynamic decision-making
  6. Common Pitfalls - Best practices and issues to avoid

Let’s start with the building blocks: parameters and variables.

Pipeline parameters

Parameters are compile-time values referenced using ${{ parameters.name }} syntax:

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

Unlike parameters, variables can be modified dynamically during pipeline execution. Variables can be defined at different scopes, each with its own visibility and lifetime:

  1. Pipeline scope: Global visibility (all stages, jobs, steps). Used for configuration values needed throughout the pipeline.
  2. Stage scope: Limited to a specific stage and its jobs. Used for stage-specific settings.
  3. Job scope: Limited to a specific job and its steps. Used for job-specific configurations.

All three scopes are shown in this example:

# Pipeline-level variables
variables:
  globalConfig: 'production'

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:

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) and 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)) }}

Template Expressions

Template expressions create reusable pipeline components with parameterized behavior:

# 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
- $${{ if eq(parameters.runTests, true) }}:
  - script: dotnet test
    displayName: 'Run Tests'

Here is how to use the template 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 ($(<expression>) or $[<expression>]) use variables to make decisions during pipeline execution:

Here are examples of runtime expressions inside conditions:

# 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].

Another way of dynamically setting a runtime variable is by using the ##vso prefix:

- script: echo "##vso[task.setvariable variable=hasUnitTests]true"

This 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 then 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

  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

Feel free to share this article or leave a comment.

comments powered by Disqus