7.3 Reusable Elements: Templates, Parameters & Variables

Key Takeaways

  • YAML templates modularize pipeline logic across step, job, and stage tiers, enabling enterprise governance, compliance enforcement, and cross-team standardization.
  • Templates can be sourced locally within the repository or externally from centralized governance repositories declared in the resources: repositories: block.
  • The extends template pattern enforces strict enterprise compliance by requiring application pipelines to inject logic into a predefined organizational template skeleton.
  • Variable syntax spans three distinct evaluation phases: compile-time template expressions (${{ ... }}), job initialization expressions ($[ ... ]), and task runtime macro expansion ($( ... )).
  • Passing output variables across jobs and stages requires setting isOutput=true via VSO logging commands and referencing them via the dependencies or stageDependencies context.
Last updated: September 2026

7.3 Reusable Elements: Templates, Parameters & Variables

As enterprise organizations scale their adoption of Azure DevOps, maintaining monolithic, copy-pasted pipeline definitions across dozens or hundreds of repositories quickly creates maintenance bottlenecks, security drifts, and compliance violations. A security update to a container scanning tool or a change in artifact signing requirements would require editing hundreds of individual YAML files.

Azure Pipelines provides enterprise-grade modularization mechanisms through YAML Templates, Typed Parameters, and a multi-tiered Variable Evaluation Engine. Mastering these constructs is fundamental to architecting scalable CI/CD pipelines and excelling on the AZ-400 exam.


1. YAML Templates Architecture: Step, Job, Stage, and Extends

Templates allow you to define reusable logic in separate YAML files and include them in multiple pipeline definitions. Templates can be structured at four architectural levels:

  1. Step Templates: Encapsulate a sequence of one or more steps (e.g., configuring an SDK, running SonarQube analysis, signing binaries).
  2. Job Templates: Encapsulate one or more complete jobs (e.g., standard build matrix, container packaging workflow).
  3. Stage Templates: Encapsulate entire stages (e.g., an entire standardized QA deployment and automated verification stage).
  4. Extends Templates (Governance): Inverts the relationship—rather than an application pipeline importing snippets, the application pipeline extends a centralized enterprise template. This ensures that organizational security gates, scanning, and compliance checks cannot be bypassed by development teams.

Step Template Example (Local Reference)

Consider a reusable step template located at .pipelines/templates/security-scan.yml:

# File: .pipelines/templates/security-scan.yml
parameters:
  - name: scanLevel
    type: string
    default: 'standard'
    values:
      - 'quick'
      - 'standard'
      - 'deep'
  - name: failOnVulnerabilities
    type: boolean
    default: true

steps:
  - script: echo "Running vulnerability scan with depth ${{ parameters.scanLevel }}..."
    displayName: 'Execute Security Scanner'
  - ${{ if eq(parameters.failOnVulnerabilities, true) }}:
    - script: echo "Enforcing zero-tolerance security gate..."
      displayName: 'Verify Security Vulnerability Gate'

Consuming the step template in the main azure-pipelines.yml:

# File: azure-pipelines.yml
trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

jobs:
  - job: BuildAndScan
    steps:
      - script: echo "Compiling application..."
      - template: .pipelines/templates/security-scan.yml
        parameters:
          scanLevel: 'deep'
          failOnVulnerabilities: true

Centralized Multi-Repository Templates

In enterprise environments, templates are maintained in a dedicated, secure repository owned by the Central DevOps or Platform Engineering team. Application repositories reference this external repository via the resources: repositories: declaration:

# File: azure-pipelines.yml (Application Repository)
resources:
  repositories:
    - repository: SharedTemplates
      type: git
      name: CorePlatform/PipelineTemplates # ProjectName/RepoName
      ref: refs/heads/main # Branch, tag, or commit

stages:
  - stage: Build
    jobs:
      - job: AppBuild
        steps:
          - checkout: self
          # Reference external template using @<repoIdentifier>
          - template: steps/dotnet-build.yml@SharedTemplates
            parameters:
              buildConfiguration: 'Release'
              enableTelemetry: true

The extends Pattern for Security and Governance

The extends template syntax is a critical AZ-400 architectural pattern. When using extends, the central template controls the execution skeleton of the pipeline, and the child pipeline can only supply approved parameters or inject steps into designated hook points.

# Central Governance Template: CoreGovernance/templates/secure-pipeline.yml
parameters:
  - name: buildSteps
    type: stepList
    default: []

stages:
  - stage: ComplianceAndBuild
    jobs:
      - job: MandatorySecurityScan
        steps:
          - script: run-mandatory-org-audit.sh
            displayName: 'Mandatory InfoSec Audit'
      - job: DeveloperBuild
        dependsOn: MandatorySecurityScan
        steps:
          # Developer build steps injected here
          - ${{ parameters.buildSteps }}
          # Central mandatory post-build validation
          - script: upload-sbom-to-central-audit.sh
            displayName: 'Upload SBOM'
# Application Repository azure-pipelines.yml using extends
resources:
  repositories:
    - repository: GovernanceRepo
      type: git
      name: InfoSec/EnterpriseTemplates

extends:
  template: templates/secure-pipeline.yml@GovernanceRepo
  parameters:
    buildSteps:
      - script: dotnet build
      - script: dotnet test

2. Template Parameters: Types, Validation, and Compile-Time Logic

Parameters are defined at the top of a template or root pipeline. Unlike variables, parameters are strongly typed, evaluated strictly at compile time, and validated before the pipeline begins execution.

Supported Parameter Types

  • string: Free-form text (can be constrained using a values enumeration list).
  • number: Numeric values.
  • boolean: true or false.
  • object: Complex YAML structures, arrays, or key-value dictionaries.
  • step / stepList: A single step or a list of steps injected dynamically.
  • job / jobList: A job or list of jobs.
  • stage / stageList: A stage or list of stages.

Parameter Validation

Parameters support explicit validation rules using the values keyword to enforce allowed options:

parameters:
  - name: environmentName
    displayName: 'Deployment Environment Target'
    type: string
    default: 'Development'
    values:
      - 'Development'
      - 'Testing'
      - 'Staging'
      - 'Production'

If a pipeline run is triggered with a value outside this allowed list, Azure Pipelines fails during compile-time validation before any agent is provisioned.


3. Variable Evaluation Phases: Compile-Time vs. Runtime

A frequent source of bugs and exam questions is conflating variable syntaxes. Azure Pipelines evaluates variables across three distinct operational phases:

[ Phase 1: Compile Time ] ──► [ Phase 2: Job Initialization ] ──► [ Phase 3: Task Execution ]
  Syntax: ${{ ... }}            Syntax: $[ ... ]                    Syntax: $( ... )
  Template Parsing & Parsing    Agent Scheduling & Conditions       In-Task Macro Expansion

Detailed Variable Syntax Comparison Matrix

SyntaxName / MechanismEvaluation TimingPrimary Context / LocationHandles Secrets?
${{ <expression> }}Template ExpressionCompile time (before pipeline runs)Template insertion, loops (${{ each }}), conditional blocks (${{ if }}), parameter parsingNo (Secrets do not exist at compile time)
$[ <expression> ]Runtime ExpressionJob initialization timeStage/job condition:, job-level variables: definitions, output variables from prior jobsNo (Not expanded into secret text)
$( <variableName> )Macro SyntaxTask execution runtime (immediately prior to step start)Task inputs, step display names, environment variablesYes (Secrets are injected and masked in logs)

Common Syntactic Pitfalls

  1. Trying to read a secret via template syntax ${{ ... }}: At compile time, secrets have not been decrypted or fetched from Key Vault. Using ${{ variables.secretKey }} produces an empty string.
  2. Using macro syntax $( ... ) in a condition expression: The pipeline parser does not expand macros inside expression functions like eq(...). Writing condition: eq('$(Build.SourceBranch)', 'refs/heads/main') will evaluate the literal text '$(Build.SourceBranch)'. You must write condition: eq(variables['Build.SourceBranch'], 'refs/heads/main').
  3. Expecting compile-time loops to iterate over runtime output: Template loops (${{ each item in ... }}) execute only once when the YAML file is parsed into memory. They cannot iterate over lists generated dynamically by earlier build steps.

4. Passing Output Variables Across Steps, Jobs, and Stages

A pipeline often needs to dynamically compute a value in one step (e.g., an image tag, a release version, an Azure resource ID) and consume it in downstream steps, jobs, or stages.

Mechanism 1: Step-to-Step Within the Same Job

To pass a variable to subsequent steps within the same job, output an Azure DevOps logging command to standard out:

echo "##vso[task.setvariable variable=DYNAMIC_VERSION]1.4.2"

In the next step on the same agent, access it as $(DYNAMIC_VERSION) or via the environment variable $DYNAMIC_VERSION.

Mechanism 2: Job-to-Job Within the Same Stage

To pass an output variable to a downstream job within the same stage:

  1. The producing step must set isOutput=true.
  2. The producing step must define a name: attribute.
  3. The consumer job must declare a dependency on the producer job using dependsOn.
  4. The consumer job maps the variable at the job level using runtime expression syntax: $[ dependencies.<JobName>.outputs['<StepName>.<VariableName>'] ].
jobs:
  - job: ProducerJob
    steps:
      - script: |
          BUILD_HASH=$(git rev-parse --short HEAD)
          echo "##vso[task.setvariable variable=appHash;isOutput=true]$BUILD_HASH"
        name: VersionStep
        displayName: 'Generate Version Hash'

  - job: ConsumerJob
    dependsOn: ProducerJob
    variables:
      # Map output variable using dependencies context
      computedHash: $[ dependencies.ProducerJob.outputs['VersionStep.appHash'] ]
    steps:
      - script: echo "Received build hash from producer: $(computedHash)"

Mechanism 3: Stage-to-Stage Across Stages

To pass an output variable across stage boundaries:

  1. The producing step sets isOutput=true and has a name: attribute.
  2. The consumer stage specifies dependsOn: <ProducerStage>.
  3. The consumer job inside the downstream stage maps the variable using the stageDependencies context: $[ stageDependencies.<ProducerStage>.<ProducerJob>.outputs['<StepName>.<VariableName>'] ].
stages:
  - stage: BuildStage
    jobs:
      - job: BuildJob
        steps:
          - script: |
              CLUSTER_IP="10.240.0.45"
              echo "##vso[task.setvariable variable=targetIp;isOutput=true]$CLUSTER_IP"
            name: NetworkStep
            displayName: 'Discover Target IP'

  - stage: DeployStage
    dependsOn: BuildStage
    jobs:
      - job: DeployJob
        variables:
          # Map cross-stage output variable
          serverIp: $[ stageDependencies.BuildStage.BuildJob.outputs['NetworkStep.targetIp'] ]
        steps:
          - script: echo "Deploying payload to destination IP: $(serverIp)"

5. Realistic Exam Scenarios & Common Traps

Scenario: Enforcing Enterprise Security Compliance via Extends

Context: An enterprise financial institution with 150 development teams mandates that every container image built in Azure Pipelines must undergo static vulnerability scanning with Prisma Cloud and generate an SBOM using Syft. Developers frequently edit their azure-pipelines.yml files to comment out scanning steps when deadlines approach. Solution: Transition the organization from step templates to the extends template pattern. Define a centralized template in an administrative repository (InfoSec/ComplianceTemplates). In the root application repositories, restrict pipeline definitions so they can only provide parameters to extends: template: secure-build.yml@ComplianceTemplates. Developers cannot modify the sequence of stages, ensuring compliance scans execute before any image is pushed to the container registry.

Common Exam Traps to Avoid

  • Trap: Omitting name: on the step generating an output variable. When calling ##vso[task.setvariable ...;isOutput=true], if the step lacks a name: attribute (e.g., name: OutputStep), the output variable will not be registered in the job's outputs dictionary, causing downstream jobs to read empty values.
  • Trap: Conflating dependencies and stageDependencies. Within the same stage, downstream jobs reference dependencies.<Job>.outputs[...]. Across different stages, you must use stageDependencies.<Stage>.<Job>.outputs[...].
  • Trap: Attempting to evaluate runtime variables at compile time. If you write ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}, template expressions are evaluated before the branch context is established during queuing. Use condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') instead.
Loading diagram...
Variable Evaluation Lifecycle & Cross-Stage Output Propagation
Test Your Knowledge

A release architect wants to set a dynamically generated semantic version number in an initial compilation stage and consume that exact version number in a downstream deployment stage. In the producer stage, the job 'BuildJob' contains a step named 'VersionStep' that executes 'echo "##vso[task.setvariable variable=SemVer;isOutput=true]1.2.3"'. How must the consumer job in the downstream stage declare and consume this variable?

A
B
C
D
Test Your Knowledge

An enterprise organization requires that all 80 microservice delivery pipelines strictly execute a corporate SonarQube quality gate and an image vulnerability scan prior to deploying any code to production. Individual development teams currently own their respective 'azure-pipelines.yml' files and have occasionally commented out these scanning steps to expedite emergency releases. What is the recommended architectural solution to prevent development teams from bypassing these mandatory checks?

A
B
C
D
Test Your Knowledge

A DevOps engineer is designing an Azure Pipelines YAML template and needs to configure conditional behavior based on the target deployment environment. The engineer must choose between template syntax '${{ if ... }}' and runtime condition syntax 'condition: ...'. Which statement correctly describes the architectural difference and appropriate usage?

A
B
C
D