Quick Answer
Classic pipelines use a GUI-based editor stored in Azure DevOps metadata, while YAML pipelines define CI/CD as code in a version-controlled file. YAML enables pull request reviews, template reuse, and multi-stage deployments in a single file.
Detailed Answer
Imagine two ways to give driving directions: a voice-guided GPS (Classic) where you click through turns on a screen, versus a written route card (YAML) that you can photocopy, annotate, version, and hand to anyone. Both get you to the destination, but the written card travels with the project and can be peer-reviewed before the trip. Classic pipelines were the original Azure DevOps experience. Build definitions use a visual task editor where you drag-and-drop tasks like NuGet Restore, MSBuild, or Docker Build. Release definitions add environments with deployment gates, approvals, and artifact triggers. The configuration is stored as JSON metadata inside Azure DevOps, not in your repository. This means pipeline changes do not go through pull requests, cannot be easily diffed, and are invisible in your Git history. YAML pipelines store the entire pipeline definition in an azure-pipelines.yml file committed alongside your source code. Every change to the pipeline goes through the same pull request workflow as application code. YAML supports multi-stage pipelines (build, test, deploy to staging, deploy to production) in a single file with conditional execution, template references, and environment approvals. The extends keyword and template repositories enable centralized governance across hundreds of pipelines. Under the hood, both pipeline types use the same agent infrastructure and task ecosystem. A Classic build task like DotNetCoreCLI@2 is the same task referenced in YAML as - task: DotNetCoreCLI@2. The difference is purely in how the orchestration is defined and stored. In production, most organizations are migrating from Classic to YAML because Microsoft has signaled Classic pipelines will not receive new features. The gotcha is that Classic Release pipelines have some features (like release gates with Azure Monitor integration and graphical deployment visualization) that require extra YAML configuration using environments and checks. Teams migrating often underestimate the effort to replicate approval workflows, variable scoping, and artifact filtering that Classic provided through the GUI.
Code Example
# Classic pipeline equivalent in YAML — azure-pipelines.yml
# This replaces a Classic Build + Release definition pair
trigger:
branches:
include:
- main
- release/*
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
displayName: 'Build payments-api'
jobs:
- job: BuildJob
steps:
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: 'src/payments-api/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build solution'
inputs:
command: 'build'
projects: 'src/payments-api/*.csproj'
arguments: '--configuration Release'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: 'tests/**/*.csproj'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: Build
jobs:
- deployment: DeployToStaging
environment: 'payments-staging'
strategy:
runOnce:
deploy:
steps:
- script: echo 'Deploying to staging environment'◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐ │ Classic Pipeline │ │ ┌──────────┐ ┌──────────────┐ ┌─────────────┐ │ │ │ Build │───→│ Release Def │───→│ Environment │ │ │ │ (GUI) │ │ (GUI) │ │ (GUI) │ │ │ └──────────┘ └──────────────┘ └─────────────┘ │ │ Stored in Azure DevOps metadata (not in Git) │ └──────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────┐ │ YAML Pipeline │ │ ┌────────────────────────────────────────────────────┐ │ │ │ azure-pipelines.yml (in Git repo) │ │ │ │ stages: Build → Test → Deploy Staging → Deploy Prod│ │ │ └────────────────────────────────────────────────────┘ │ │ Versioned │ PR-reviewed │ Templated │ Multi-stage │ └──────────────────────────────────────────────────────────┘
Quick Answer
Create an azure-pipelines.yml file in your repo root defining a trigger, agent pool, and build steps. Azure DevOps detects the file and offers to create the pipeline, or you can use az pipelines create to wire it up via CLI.
Detailed Answer
Think of a YAML pipeline file like a recipe card you tape to your refrigerator. Anyone who opens the fridge (clones the repo) immediately knows exactly how to cook the dish (build the project) without asking the chef (searching through a GUI for hidden configuration). The minimum viable YAML pipeline has three elements: a trigger that specifies which branches activate the pipeline, a pool that defines which agent runs the work, and steps that list the actual build commands. For a .NET project, the steps typically include restore, build, test, and publish. For Node.js, they include npm install, lint, test, and build. Azure DevOps provides starter templates when you create a new pipeline through the portal, automatically detecting your project type. When the pipeline runs, Azure DevOps provisions a fresh agent from the specified pool. Microsoft-hosted agents come pre-installed with common SDKs (.NET, Node.js, Python, Java, Go) and tools (Docker, kubectl, Terraform). The agent clones your repository, executes each step sequentially, and reports results back. Build artifacts like compiled binaries or Docker images can be published for downstream stages. In production, even a basic pipeline should include caching for package restore (to speed up builds from 5 minutes to 90 seconds), test result publishing (so failures appear in the PR UI), and branch filters (to avoid running on documentation-only branches). The variables section externalizes configuration like SDK versions so upgrades require changing one line. The most common gotcha for beginners is indentation errors in YAML causing cryptic parse failures. Azure DevOps provides a YAML editor with IntelliSense in the portal, but many teams prefer editing locally with the Azure Pipelines VS Code extension that provides schema validation. Another frequent issue is the agent not having a required tool version — use the UseDotNet@2 or NodeTool@0 tasks to explicitly install the version you need rather than relying on whatever is pre-installed.
Code Example
# azure-pipelines.yml — Basic .NET build pipeline for payments-api
trigger:
- main
- feature/*
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.0.x'
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
packageType: 'sdk'
version: '$(dotnetVersion)'
- task: DotNetCoreCLI@2
displayName: 'Restore NuGet packages'
inputs:
command: 'restore'
projects: 'src/payments-api/**/*.csproj'
feedsToUse: 'select'
vstsFeed: 'contoso-internal-feed'
- task: DotNetCoreCLI@2
displayName: 'Build payments-api'
inputs:
command: 'build'
projects: 'src/payments-api/**/*.csproj'
arguments: '--configuration $(buildConfiguration) --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: 'tests/**/*.csproj'
arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage"'
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
---
# azure-pipelines.yml — Basic Node.js pipeline for fraud-detector
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: 'Use Node.js 20.x'
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: 'Install dependencies (clean)'
- script: npm run lint
displayName: 'Run ESLint'
- script: npm run test -- --coverage
displayName: 'Run Jest tests with coverage'
- script: npm run build
displayName: 'Build production bundle'◈ Architecture Diagram
┌─────────────────────────────────────────────────┐ │ Pipeline Execution Flow │ │ │ │ ┌─────────┐ ┌─────────┐ ┌──────────────┐ │ │ │ Trigger │──→│ Agent │──→│ Clone Repo │ │ │ │ (push) │ │ (pool) │ │ (checkout) │ │ │ └─────────┘ └─────────┘ └──────┬───────┘ │ │ │ │ │ ↓ │ │ ┌─────────┐ ┌─────────┐ ┌──────────────┐ │ │ │Publish │←──│ Test │←──│ Build │ │ │ │Artifacts│ │ Run │ │ Compile │ │ │ └─────────┘ └─────────┘ └──────────────┘ │ └─────────────────────────────────────────────────┘
Quick Answer
Multi-stage YAML pipelines define build, test, and deploy stages in a single azure-pipelines.yml file. Approval gates are configured on Azure DevOps Environments, requiring manual approval or automated checks before the deployment job targeting that environment can proceed.
Detailed Answer
Think of a relay race with checkpoints. Each runner (stage) must complete their leg before the next starts, and at certain checkpoints (environments), a judge (approver) must wave the green flag before the next runner can go. The entire race plan is written down in advance (YAML), but human judgment controls progression at critical points. Multi-stage YAML pipelines replaced the old Classic release pipelines with a code-as-configuration approach. A single YAML file defines multiple stages — typically Build, Test, Deploy-Dev, Deploy-Staging, Deploy-Prod. Each stage contains jobs, and each job contains steps. Stages run sequentially by default but can be configured with dependsOn to run in parallel or in custom orders. The deployment jobs reference Azure DevOps Environments. Environments are the key to approval gates. You create environments (dev, staging, production) in Azure DevOps under Pipelines > Environments. On each environment, you configure Approvals and Checks: manual approvals (specific users or groups must approve), business hours check (only deploy during work hours), branch control (only allow deployments from the main branch), and exclusive lock (prevent concurrent deployments). When a pipeline stage targets an environment with approvals, it pauses and notifies the approvers. At production scale, teams configure progressively stricter gates. Dev deploys automatically on every PR merge. Staging requires one approval from the QA lead. Production requires two approvals from different teams (dev lead + ops lead), business hours enforcement, and a branch control check that only allows the main branch. Templates extract common stage definitions into reusable files, so 50 pipelines share the same deploy-to-production stage with identical gates. The non-obvious gotcha is that environment approvals apply to the environment resource, not the pipeline. If you rename or recreate an environment, you lose all configured approvals and must set them up again. Also, approval timeouts default to 30 days — if nobody approves within that window, the pipeline run expires. Teams should set shorter timeouts (24-48 hours) and configure approval notifications to avoid stale pipeline runs accumulating.
Code Example
# azure-pipelines.yml — Multi-stage pipeline with approval gates
trigger:
branches:
include: [main] # Only trigger on main branch
stages:
- stage: Build
jobs:
- job: BuildApp
pool:
vmImage: ubuntu-latest # Microsoft-hosted agent
steps:
- script: dotnet build --configuration Release # Build the .NET application
- task: PublishBuildArtifacts@1 # Publish artifacts for deploy stages
inputs:
pathtoPublish: $(Build.ArtifactStagingDirectory)
artifactName: drop
- stage: DeployDev
dependsOn: Build # Runs after Build completes
jobs:
- deployment: DeployToDev
environment: dev # No approvals configured — auto deploys
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying to dev" # Deploy steps here
- stage: DeployProd
dependsOn: DeployDev # Runs after dev succeeds
jobs:
- deployment: DeployToProd
environment: production # Has manual approval configured
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying to production" # Deploy steps here