How do you handle pipeline variables, variable groups, and runtime parameters in YAML pipelines?
Quick Answer
Pipeline variables store values at different scopes (pipeline, stage, job), variable groups centralize shared secrets and configuration in the Library, and runtime parameters provide typed inputs with validation at queue time. Variables use $() macro syntax for runtime access, ${{ }} for compile-time template expressions, and $[ ] for runtime expressions in conditions.
Detailed Answer
Think of the variable system in Azure DevOps like a layered configuration management system in a large application. Runtime parameters are like command-line arguments — they are provided when you start the program and cannot change during execution. Pipeline variables are like environment variables — they exist at different scopes and can be set or overridden during execution. Variable groups are like external configuration services — they centralize values used across multiple applications and can be updated independently without modifying each consumer.
Pipeline variables are defined inline in the YAML at the pipeline, stage, or job level. Variables at a higher scope are inherited by lower scopes but can be overridden. A variable defined at the pipeline level is available in all stages and jobs, while a variable defined at the job level is only available within that job. Variables can be static (defined in YAML) or dynamic (set during execution using the ##vso[task.setvariable] logging command). Dynamic variables set in one step are available in subsequent steps of the same job. To share dynamic variables across jobs, you must declare them as output variables and reference them using the dependencies syntax.
Variable groups reside in the Azure DevOps Library and contain name-value pairs that can be shared across multiple pipelines. They solve the problem of updating a value once and having all consuming pipelines use the new value without code changes. Variable groups can be plain (values stored in Azure DevOps) or linked to Azure Key Vault for enterprise secrets management. Pipeline YAML references variable groups by name, and Azure DevOps resolves the values at runtime. Variable groups have their own permission model: you can restrict which pipelines are authorized to use a group, preventing unauthorized access to secrets.
Runtime parameters differ from variables in critical ways. Parameters are typed (string, boolean, number, object, step, stage), validated at queue time, and resolved during template compilation. This means parameters can control pipeline structure: a boolean parameter can conditionally include or exclude entire stages using ${{ if }} expressions. Parameters also support allowed value lists that constrain what callers can specify, providing input validation that variables lack. When a pipeline is triggered manually through the UI, parameters appear as form fields with their specified types, making them user-friendly for operators who need to customize pipeline behavior.
The three expression syntaxes serve different evaluation phases. Template expressions (${{ }}) evaluate when Azure DevOps compiles the YAML, before any agent runs. They can modify pipeline structure by conditionally including stages, jobs, or steps. Macro expressions ($()) evaluate at runtime when a step executes and are used for simple variable substitution in task inputs. Runtime expressions ($[ ]) evaluate when the pipeline determines whether to run a stage or job and support functions like eq(), and(), and dependency references. Confusing these causes subtle bugs: using $() in a condition does not work because conditions need $[ ] syntax.
The production gotcha is variable precedence and the secret masking system. Variables set at queue time override YAML-defined variables, and variable groups at a lower scope override higher-scope groups. This precedence can cause confusion when a variable appears to have the wrong value. For secrets, any variable marked as secret is masked in logs (replaced with ***) but this masking only catches exact string matches. If a secret value is base64-encoded, URL-encoded, or split across multiple log lines, the masking fails and the secret leaks. Teams must never echo or transform secrets in ways that alter the string representation. Another issue is the 'settable at queue time' checkbox: if disabled, manual pipeline runs cannot override the variable, but triggered runs always use the YAML value regardless of this setting.