When Azure Pipelines reports "A template expression is not allowed in this context", a ${{ }} expression sits in a key the template engine does not expand: Microsoft documents that expressions are only expanded for stages, jobs, steps and containers. The usual culprits are trigger, a resources entry, a top-level pool and a parameter default that reads another parameter, and you fix this Azure Pipelines template expression not allowed error by moving the expression into a stage, job or step, using a literal, or computing the value with coalesce where it is used.

Each syntax is read at a different moment of one run, and sees only what exists by then. The error is the first phase refusing a key it does not expand.
The message comes with a file, a line and a column, and nothing else. It does not say which keys would have accepted the expression, and the three expression syntaxes Azure Pipelines offers look similar enough that it is easy to reach for the wrong one.
This page answers the error first: the exact text, the rule behind it, and the four places it is reported in public issues, each with its fix. Then it maps compile-time ${{ }}, runtime $[ ] and macro $( ) to GitHub Actions, for anyone reading it halfway through a migration from Azure Pipelines.
The error, as it is reported
These three lines are copied from public issues: microsoft/azure-pipelines-yaml #479 (an ${{ if }} under resources), #549 (an ${{ if }} inside a top-level pool), and microsoft/azure-pipelines-agent #1581 (a parameter default that reads another parameter). The path, line and column point at the key, not at the expression.
The rule behind all three is one sentence in Microsoft's template expressions page: "Expressions are only expanded for stages, jobs, steps, and containers (inside resources)." The same page adds that you cannot use an expression inside trigger or a resource like repositories, with one later exception covered below. Anything outside that list is parsed as a literal, and a literal key that starts with ${{ is refused.
```Log lines from the three issues named above
/Deployment/Pipelines/release-main.yaml (Line: 20, Col: 5): A template expression is not allowed in this context
/.pipelines/ci.yml (Line: 35, Col: 3): A template expression is not allowed in this context
_core.yml (Line: 7, Col: 19): A template expression is not allowed in this context
## Three syntaxes, three moments Microsoft's variables page defines when each one is processed and where it may appear. The column that explains most errors is the second: only a template expression may stand in a key, and only in the sections the template engine expands. | Syntax | Processed | Where it expands | Reads | Not found renders as | | --- | --- | --- | --- | --- | | `${{ variables.var }}` template expression | Compile time, before runtime starts | Key or value, inside `stages`, `jobs`, `steps` and `containers` | `parameters` and statically defined `variables` | Empty string | | `$[ variables.var ]` runtime expression | Runtime | Value only, and it must take up the entire right side | More `variables`, including `dependencies` outputs; no parameters | Empty string | | `$(var)` macro | Runtime, before a task executes | Value only, in task inputs | Variables, including ones a script set with `setvariable` | Prints `$(var)` unchanged | > Microsoft Learn, Define variables, "Understand variable syntax", and Expressions, both read 2026-11-12. ## Cause 1: an expression in trigger or resources An Azure Pipelines maintainer answered #479 directly: expressions are not allowed in `resources` or `triggers`, "to break several chicken-and-egg problems". The pipeline has to know its triggers and resources before it can be expanded, so they cannot depend on the expansion. Two exceptions exist. Container resources accept template expressions. And Microsoft's YAML schema for repository resources now states that template expressions are supported for `ref`, but not for `name`, on Azure DevOps Services and Server 2022.1 and later. A conditional `${{ if }}` block under `resources`, as in #479, is still refused. Use a runtime parameter for `ref`, and literals everywhere else. ```azure-pipelines.yml parameters: - name: toolsRef type: string default: refs/heads/main resources: repositories: - repository: tools type: git name: Platform/tools # literal: name takes no expression ref: ${{ parameters.toolsRef }} # allowed: ref takes a template expression Cause 2: a top-level pool or its demands
A pool: at the root of the file sits outside stages, jobs and steps, so an expression in it, or in its demands, is refused. That is #549, and it is still being hit: microsoft/mssql-rs #672, opened on 2026-09-29, reports the same error for imageOverride -equals ${{ parameters.linuxImage }} in a top-level pool.demands. That project fixed it by writing the image as a literal.
If the value has to vary, move pool: into the job, where it is inside jobs and therefore expanded. The same applies to a job template: put the pool in the template's job, not at the root of the calling file.
parameters: - name: linuxImage type: string default: ubuntu-24.04 jobs: - job: build pool: name: default demands: - imageOverride -equals ${{ parameters.linuxImage }} steps: - script: make Cause 3: a parameter default that reads another parameter
Issue #1581 is the parameters block itself: packagesToPack: ${{ parameters.projects }} as a default. Defaults are literals. The documented way to fall back to another parameter is coalesce at the point of use, which returns its first non-empty argument.
One caveat from Microsoft's own note on coalesce: in the Run Pipeline pane, Azure DevOps requires a non-empty value for a string parameter whose default is an empty string, so a manually queued run never falls back. Running from the pipeline editor respects the empty default.
parameters: - name: projects type: string default: 'Source/*.csproj' - name: projectsToPack type: string default: '' steps: - script: dotnet pack ${{ coalesce(parameters.projectsToPack, parameters.projects) }} Cause 4: a runtime value in a compile-time slot
The quieter variant does not error at all. A template expression reads variables as they were in the file, so ${{ variables.one }} still prints initialValue after a script set one with setvariable; Microsoft's variables page shows exactly that. Variables stored on the pipeline in the UI, values given when a run is queued and output variables do not exist yet either.
When a decision depends on something a job produced, make it at runtime: write an output variable, then read it through dependencies in a condition or a $[ ] variable. A condition: on a - template: line is refused with "Unexpected value 'condition'", per microsoft/azure-pipelines-agent #1749, so put the condition on the job inside the template instead.
jobs: - job: A steps: - bash: echo "##vso[task.setvariable variable=shouldrun;isOutput=true]true" name: printvar - job: B dependsOn: A condition: and(succeeded(), eq(dependencies.A.outputs['printvar.shouldrun'], 'true')) steps: - script: echo hello from B The same ideas in GitHub Actions
GitHub Actions has one syntax, ${{ }}, and no template phase. When an expression is evaluated depends on the key it is in: a job's if: is evaluated by GitHub before the job is sent to a runner, a step's if: and run: on the runner. GitHub's context availability table is the equivalent of Azure's "where it expands" column: jobs.<job_id>.if can read github, needs, vars and inputs, but not secrets or env.
| Azure Pipelines | GitHub Actions |
|---|---|
${{ parameters.x }} | ${{ inputs.x }} from workflow_dispatch or workflow_call |
${{ variables.x }}, static | ${{ env.X }}, or ${{ vars.X }} for a configuration variable |
${{ if }} and ${{ each }} insertion | No equivalent: if: on a job or step, and strategy.matrix, with fromJSON for a dynamic list |
$[ dependencies.A.outputs['step.var'] ] | ${{ needs.A.outputs.var }}, after job A declares it under outputs: |
condition: eq(variables.x, 'a') | if: env.X == 'a' on a step; a job's if: cannot read env |
$(var) macro in a script | $VAR in the shell, or ${{ env.VAR }} |
##vso[task.setvariable variable=x] | echo "x=value" >> "$GITHUB_ENV" |
##vso[task.setvariable variable=x;isOutput=true] | echo "x=value" >> "$GITHUB_OUTPUT" |
Yes, GitHub Actions has the same error
GitHub Actions reports the same sentence. GitHub community discussion #38492 reports "A template expression is not allowed in this context" for ${{ true && '' || 'job2' }} inside needs:, which only takes a literal list. The workaround given there is two jobs with mutually exclusive if: conditions.
The keys to watch are needs: and the uses: of a reusable workflow, which the workflow schema in actions/runner gives no contexts at all. The parser checks names before it checks the key, so the message depends on the expression: one that names no context gets the sentence above, and one that reads a context, such as inputs or github, is reported as "Unrecognized named-value: 'github'" first, as the same discussion shows. That message also appears where an expression is allowed but the context is not, covered on the env context page.
on: workflow_dispatch: inputs: with_extra: type: boolean default: false jobs: build: runs-on: ubuntu-latest steps: - run: make build extra: if: inputs.with_extra runs-on: ubuntu-latest steps: - run: make extra deploy: # needs: [build, ${{ inputs.with_extra && 'extra' || '' }}] refused: needs takes no contexts needs: [build] if: '!inputs.with_extra' runs-on: ubuntu-latest steps: - run: ./deploy.sh deploy-after-extra: needs: [build, extra] if: inputs.with_extra runs-on: ubuntu-latest steps: - run: ./deploy.sh Context lists from
src/Sdk/WorkflowParser/workflow-v1.0.jsonand the order of the two checks fromsrc/Sdk/WorkflowParser/ObjectTemplating/TemplateReader.cs, both in actions/runner, read 2026-11-12. Both messages are quoted from community discussion #38492.
FAQ
What is the difference between ${{ }}, $[ ] and $( ) in Azure Pipelines?
${{ }} is a template expression, expanded at compile time from parameters and static variables, and it may appear as a key or a value. $[ ] is a runtime expression for variables and conditions, and must be the entire right side. $( ) is macro syntax, replaced just before each task runs, and left as written when the variable does not exist.
Can I use a variable in the resources section of an Azure pipeline?
Not a runtime or macro variable. Microsoft's variables page states that macro syntax is not expanded in keywords that resolve at compile time, such as resources and trigger. A template expression, typically a runtime parameter, is accepted in a repository resource's ref and in container resources, but not in a repository name.
Why does my template expression show the old value of a variable?
Because it was evaluated before any step ran. A template expression reads the variable as defined in the file, so a value set later with ##vso[task.setvariable] is visible to $(var) and to runtime expressions, never to ${{ variables.var }}.
Can I put a condition on a template in Azure Pipelines?
Not with condition: on the - template: line, which is refused as an unexpected value. Wrap the template reference in ${{ if }} when the decision can be made at compile time, or put a condition: on the jobs or steps inside the template when it depends on runtime state.
References
- Microsoft Learn: template expressions in Azure Pipelines (verified 2026-11-12)
- Microsoft Learn: define variables, macro, template and runtime syntax (verified 2026-11-12)
- Microsoft Learn: resources.repositories.repository, template expressions in ref (verified 2026-11-12)
- microsoft/azure-pipelines-yaml #479: Allow if expressions in resources section (verified 2026-11-12)
- GitHub Docs: contexts, context availability (verified 2026-11-12)


