Azure Pipelines Templates Guide
This guide covers how to create and use templates in Azure Pipelines for reusable and maintainable pipeline configurations.
How to Use This Guide
Use this deterministic sequence:
- Choose template type (
step,job,stage, orvariables) based on repetition level. - Start from the corresponding example in this file.
- Keep parameters explicit, typed, and minimally scoped.
- Add runtime
conditionlogic at stage/job level for branch or environment gating. - Validate the final root pipeline and template files.
Fallback behavior:
- If template complexity adds more risk than value, inline the logic in the root pipeline and note the reason.
- If shared template repositories are not available, use local
templates/files and keep paths explicit. - If template expression behavior is uncertain, use simpler runtime conditions and document assumptions.
What Are Templates?
Templates allow you to define reusable content, logic, and parameters in YAML pipelines. They promote DRY (Don't Repeat Yourself) principles and make pipelines more maintainable.
Template Types
1. Step Templates
Reusable sets of steps.
template: templates/build-steps.yml
steps:
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: 'Install dependencies'
- script: npm run build
displayName: 'Build application'
- script: npm test
displayName: 'Run tests'Usage:
jobs:
- job: Build
steps:
- template: templates/build-steps.yml2. Job Templates
Reusable job definitions.
templates/test-job.yml
parameters:
- name: nodeVersion
type: string
default: '20'
- name: osImage
type: string
default: 'ubuntu-22.04'
jobs:
- job: Test_Node_${{ parameters.nodeVersion }}
displayName: 'Test on Node ${{ parameters.nodeVersion }}'
pool:
vmImage: ${{ parameters.osImage }}
steps:
- task: NodeTool@0
inputs:
versionSpec: ${{ parameters.nodeVersion }}
- script: npm ci
displayName: 'Install dependencies'
- script: npm test
displayName: 'Run tests'Usage:
stages:
- stage: Test
jobs:
- template: templates/test-job.yml
parameters:
nodeVersion: '18'
osImage: 'ubuntu-22.04'
- template: templates/test-job.yml
parameters:
nodeVersion: '20'
osImage: 'ubuntu-22.04'3. Stage Templates
Reusable stage definitions.
templates/deploy-stage.yml
parameters:
- name: environment
type: string
- name: dependsOn
type: object
default: []
stages:
- stage: Deploy_${{ parameters.environment }}
displayName: 'Deploy to ${{ parameters.environment }}'
${{ if gt(length(parameters.dependsOn), 0) }}:
dependsOn: ${{ parameters.dependsOn }}
jobs:
- deployment: Deploy
displayName: 'Deploy Application'
environment: ${{ parameters.environment }}
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying to ${{ parameters.environment }}"
displayName: 'Deploy'Usage:
stages:
- stage: Build
jobs: [...]
- template: templates/deploy-stage.yml
parameters:
environment: 'staging'
- template: templates/deploy-stage.yml
parameters:
environment: 'production'
dependsOn:
- Deploy_staging4. Variable Templates
Reusable variable definitions.
templates/variables-common.yml
variables:
nodeVersion: '20'
buildConfiguration: 'Release'
artifactName: 'drop'Usage:
variables:
- template: templates/variables-common.yml
- name: customVariable
value: 'customValue'Template Parameters
Parameter Types
parameters:
# String
- name: environmentName
type: string
default: 'dev'
# Number
- name: timeout
type: number
default: 30
# Boolean
- name: runTests
type: boolean
default: true
# Object (list)
- name: environments
type: object
default:
- dev
- staging
- prod
# Object (dictionary)
- name: settings
type: object
default:
debug: true
verbose: false
# Step list
- name: buildSteps
type: stepList
default: []
# Job list
- name: testJobs
type: jobList
default: []
# Stage list
- name: deployStages
type: stageList
default: []Parameter Validation
parameters:
- name: environment
type: string
values: # Restrict to specific values
- dev
- staging
- production
- name: version
type: string
default: '1.0.0'Using Parameters
# String interpolation
steps:
- script: echo "Deploying to ${{ parameters.environment }}"
displayName: 'Deploy to ${{ parameters.environment }}'
# Conditional logic
- ${{ if eq(parameters.runTests, true) }}:
- script: npm test
displayName: 'Run tests'
# Object iteration
- ${{ each env in parameters.environments }}:
- script: echo "Deploying to ${{ env }}"
displayName: 'Deploy to ${{ env }}'Template Expressions
Conditional Insertion
parameters:
- name: runTests
type: boolean
default: true
steps:
- script: npm run build
displayName: 'Build'
- ${{ if eq(parameters.runTests, true) }}:
- script: npm test
displayName: 'Run tests'
- ${{ if ne(parameters.runTests, true) }}:
- script: echo "Skipping tests"
displayName: 'Skip tests'Iteration
parameters:
- name: nodeVersions
type: object
default:
- '18'
- '20'
- '22'
strategy:
matrix:
${{ each version in parameters.nodeVersions }}:
Node_${{ version }}:
nodeVersion: ${{ version }}Each with Key-Value Pairs
parameters:
- name: environments
type: object
default:
dev:
url: https://dev.example.com
staging:
url: https://staging.example.com
prod:
url: https://prod.example.com
stages:
- ${{ each env in parameters.environments }}:
- stage: Deploy_${{ env.key }}
jobs:
- job: Deploy
variables:
targetUrl: ${{ env.value.url }}
steps:
- script: echo "Deploying to ${{ env.value.url }}"Extends Template
The extends keyword allows you to extend an entire pipeline template.
templates/secure-pipeline.yml
parameters:
- name: buildSteps
type: stepList
default: []
stages:
- stage: Build
jobs:
- job: SecurityScan
steps:
- script: echo "Running security scan"
- job: Build
steps:
- script: echo "Pre-build checks"
- ${{ each step in parameters.buildSteps }}:
- ${{ step }}
- script: echo "Post-build checks"azure-pipelines.yml
extends:
template: templates/secure-pipeline.yml
parameters:
buildSteps:
- script: npm ci
displayName: 'Install dependencies'
- script: npm run build
displayName: 'Build application'Template Inclusion
Local Templates
# Relative path from current file
- template: templates/build-steps.yml
# Relative path with parameters
- template: ../shared/deploy.yml
parameters:
environment: productionTemplates from Other Repositories
resources:
repositories:
- repository: templates
type: github
name: myorg/pipeline-templates
ref: refs/heads/main
stages:
- template: build-stage.yml@templates
parameters:
projectName: myappTemplates from Different Branches
resources:
repositories:
- repository: templates
type: git
name: MyProject/Templates
ref: refs/heads/v2
jobs:
- template: ci-job.yml@templatesAdvanced Template Patterns
Matrix Build Template
templates/matrix-test.yml
parameters:
- name: operatingSystems
type: object
default:
- ubuntu-22.04
- windows-2022
- macOS-12
- name: nodeVersions
type: object
default:
- '18'
- '20'
jobs:
- job: Test
strategy:
matrix:
${{ each os in parameters.operatingSystems }}:
${{ each version in parameters.nodeVersions }}:
${{ os }}_Node_${{ version }}:
imageName: ${{ os }}
nodeVersion: ${{ version }}
pool:
vmImage: $(imageName)
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- script: npm testConditional Stage Template
templates/optional-deploy.yml
parameters:
- name: shouldDeploy
type: boolean
default: false
- name: environment
type: string
stages:
- ${{ if eq(parameters.shouldDeploy, true) }}:
- stage: Deploy_${{ parameters.environment }}
jobs:
- deployment: Deploy
environment: ${{ parameters.environment }}
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying"Nested Templates
templates/full-pipeline.yml
parameters:
- name: runTests
type: boolean
default: true
stages:
- stage: Build
jobs:
- template: build-job.yml # Nested template
- ${{ if eq(parameters.runTests, true) }}:
- template: test-stage.yml # Nested template
- template: deploy-stage.yml # Nested template
parameters:
environment: productionTemplate Best Practices
1. Parameter Documentation
# templates/deploy.yml
# Deploys application to specified environment
# Parameters:
# environment: Target environment (dev, staging, prod)
# version: Application version to deploy
# approvalReminder: Show informational reminder to configure environment approvals/checks
parameters:
- name: environment
type: string
displayName: 'Target Environment'
- name: version
type: string
default: '$(Build.BuildId)'
displayName: 'Application Version'
- name: approvalReminder
type: boolean
default: true
displayName: 'Show Approval Reminder'Environment approvals/checks are enforced in Azure DevOps Environment settings, not by a YAML boolean parameter.
2. Default Values
Always provide sensible defaults for parameters.
parameters:
- name: buildConfiguration
type: string
default: 'Release'
- name: runTests
type: boolean
default: true3. Template Validation
Use parameter restrictions to validate inputs.
parameters:
- name: environment
type: string
values:
- dev
- staging
- production4. Template Organization
templates/
├── stages/
│ ├── build-stage.yml
│ ├── test-stage.yml
│ └── deploy-stage.yml
├── jobs/
│ ├── build-job.yml
│ └── test-job.yml
├── steps/
│ ├── npm-build.yml
│ └── docker-build.yml
└── variables/
├── common.yml
└── production.yml5. Versioning Templates
Use tags or branches to version your template repository.
resources:
repositories:
- repository: templates
type: github
name: myorg/pipeline-templates
ref: refs/tags/v2.1.0 # Specific version tagCommon Template Patterns
Build and Test Template
templates/build-and-test.yml
parameters:
- name: projectPath
type: string
default: '.'
- name: nodeVersion
type: string
default: '20'
steps:
- task: NodeTool@0
inputs:
versionSpec: ${{ parameters.nodeVersion }}
- task: Cache@2
inputs:
key: 'npm | "$(Agent.OS)" | ${{ parameters.projectPath }}/package-lock.json'
path: $(Pipeline.Workspace)/.npm
- script: npm ci --cache $(Pipeline.Workspace)/.npm
workingDirectory: ${{ parameters.projectPath }}
displayName: 'Install dependencies'
- script: npm run build
workingDirectory: ${{ parameters.projectPath }}
displayName: 'Build'
- script: npm test
workingDirectory: ${{ parameters.projectPath }}
displayName: 'Test'Docker Build Template
templates/docker-build.yml
parameters:
- name: dockerfilePath
type: string
default: 'Dockerfile'
- name: imageName
type: string
- name: imageTag
type: string
default: '$(Build.BuildId)'
steps:
- task: Docker@2
displayName: 'Build Docker image'
inputs:
command: 'build'
repository: ${{ parameters.imageName }}
dockerfile: ${{ parameters.dockerfilePath }}
tags: |
${{ parameters.imageTag }}
$(Build.SourceVersion)
- task: Docker@2
displayName: 'Push Docker image'
inputs:
command: 'push'
repository: ${{ parameters.imageName }}
tags: |
${{ parameters.imageTag }}
$(Build.SourceVersion)Deployment Approval Template
templates/deploy-with-approval.yml
parameters:
- name: environment
type: string
- name: serviceConnection
type: string
stages:
- stage: Deploy_${{ parameters.environment }}
jobs:
- deployment: Deploy
environment: ${{ parameters.environment }} # Approval configured in environment
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: ${{ parameters.serviceConnection }}
appName: 'myapp-${{ parameters.environment }}'Debugging Templates
View Expanded Template
Use the Azure DevOps UI to view the fully expanded YAML after template processing.
Template Syntax Validation
# Use template expressions for debugging
- script: echo "Environment: ${{ parameters.environment }}"
displayName: 'Debug: Show parameters'Summary
Templates are powerful for:
- Reusability: Write once, use many times
- Maintainability: Update in one place
- Consistency: Enforce standards across pipelines
- Modularity: Break complex pipelines into manageable pieces
Key takeaways:
- Use parameters for flexibility
- Provide sensible defaults
- Document your templates
- Organize templates logically
- Version your template repositories
- Test templates thoroughly
Done Criteria
Template design is complete when:
- Selected template type matches the requested reuse scope.
- Parameters are typed and defaults are explicit.
- Branch/environment gating is implemented with deterministic runtime conditions.
- Root pipeline and template composition can be validated without path ambiguity.