# Azure Pipelines: "A template expression is not allowed in this context"

> Fix the Azure Pipelines template expression not allowed error: where ${{ }}, $[ ] and $( ) expand, the four causes, and the GitHub Actions mapping.

Source: https://latchkey.dev/learn/migrate/azure-pipelines-template-expressions  
Updated: 2026-11-12

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.

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](/learn/migrate/azure-pipelines-to-github-actions).

## 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.

```azure-pipelines.yml
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.

```azure-pipelines.yml
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.

```azure-pipelines.yml
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](/learn/github-actions/gha-bad-expression-context-access).

```.github/workflows/deploy.yml
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.json` and the order of the two checks from `src/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)](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/template-expressions)
- [Microsoft Learn: define variables, macro, template and runtime syntax (verified 2026-11-12)](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables)
- [Microsoft Learn: resources.repositories.repository, template expressions in ref (verified 2026-11-12)](https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/resources-repositories-repository)
- [microsoft/azure-pipelines-yaml #479: Allow if expressions in resources section (verified 2026-11-12)](https://github.com/microsoft/azure-pipelines-yaml/issues/479)
- [GitHub Docs: contexts, context availability (verified 2026-11-12)](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#context-availability)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
