In an Azure Pipelines pool: block, what is the difference between setting vmImage and setting name together with demands?
answer
- one names an image, the other names a pool
- agents advertise what they can do
- only two comparison forms exist
- a bad filter queues rather than fails
- installing a tool is not enough on its own
basics
~20 svmImage asks for a Microsoft-hosted agent built fresh from that image for the job. name selects a specific agent pool — typically self-hosted — and demands filters which agents in it are eligible by matching against the capabilities each agent reports.
solid answer
~50 s`pool: vmImage: ubuntu-latest` requests a **Microsoft-hosted** agent: Azure provisions a clean VM from a maintained image, runs the one job on it, and discards it. You get preinstalled toolchains and no maintenance, but no control over what is installed and no route into a private network. `pool: name: MyPool` selects a named pool you own, and `demands:` narrows it further. Every agent reports **capabilities** — system ones discovered automatically, such as `Agent.OS` and installed software, plus user-defined ones set on the agent in the pool's settings. A demand is matched against those, and only two forms exist: existence (`demands: docker`) and equality (`demands: Agent.OS -equals Linux`). There is no greater-than, no regex, no expression functions. If no agent in the pool satisfies every demand, the job does not fail fast — it sits queued waiting for a match and eventually times out, which is why a typo'd demand looks like a hung pipeline rather than a broken one.
code
yaml · 17 linesjobs:
- job: FastBuild
pool:
vmImage: ubuntu-22.04
steps:
- script: npm ci && npm run build
- job: IntegrationOnPrivateNetwork
dependsOn: FastBuild
pool:
name: OnPremBuilders
demands:
- docker
- Agent.OS -equals Linux
- network -equals internal
steps:
- script: ./run-integration-tests.shgo deeper
Know that pool decides where a job runs, that vmImage requests a Microsoft-hosted machine created fresh for the job, and that a named pool means agents your team runs.
Explain capabilities versus demands, the two matching forms available, and why an unsatisfiable demand leaves the job queued instead of failing. Know that capabilities refresh only when the agent restarts.
Show judgment about which jobs justify a self-hosted pool at all — private network reach, licensed tooling, warm caches — and how you keep the pool's capability metadata honest as machines are rebuilt and tools change.
Own the fleet decision: how many pools to run, how their cost and patching burden compares with hosted parallelism, and how capability naming stays a usable contract rather than folklore only one engineer can decode.
## `pool` selects where a job runs Because the job is the unit an agent is allocated to, `pool` is fundamentally a job-level property, even though you may write it once at the root or on a stage as an inherited default. A job-level `pool` overrides whatever it inherited, so one stage can happily run a Linux job and a Windows job side by side. ```yaml jobs: - job: Linux pool: vmImage: ubuntu-latest steps: [ { script: uname -a } ] - job: Windows pool: vmImage: windows-latest steps: [ { powershell: $PSVersionTable } ] ``` ## `vmImage`: Microsoft-hosted Naming a `vmImage` requests an agent from Microsoft's hosted pool. Azure creates a virtual machine from the named image, runs exactly one job on it, and destroys it afterwards. Image names include `ubuntu-latest`, `windows-latest` and `macos-latest`, plus pinned variants such as `ubuntu-22.04` — and pinning is worth doing, because `*-latest` moves when Microsoft rotates the image, which is a routine source of "nothing changed and the build broke". What you get: a maintained toolchain, no machines to patch, and a genuinely clean starting state every time. What you give up: control of the installed software, the machine's size, and any network position — a hosted agent sits on Microsoft's network and cannot see a private subnet unless you deliberately build a path to it. One more thing hosted agents lack is `demands` in any meaningful sense: the image *is* the selector. ## `name` plus `demands`: your own pool ```yaml pool: name: LinuxBuilders demands: - docker # capability must exist - Agent.OS -equals Linux # capability must equal a value ``` Here you are choosing a pool of agents you registered and maintain. Each agent publishes a set of **capabilities**: - **System capabilities** are discovered by the agent when it starts — the operating system, the architecture, environment variables, and the presence of certain installed tools. They refresh when the agent restarts. - **User-defined capabilities** are key/value pairs you add to a specific agent in the pool's settings, and they exist precisely so you can express things the agent cannot discover: `gpu=true`, `licence=matlab`, `network=pci-zone`. A **demand** is a predicate over those capabilities, and the grammar is deliberately tiny: | Form | Meaning | |---|---| | `myCapability` | the capability exists, with any value | | `myCapability -equals value` | the capability exists and equals exactly this string | That is the whole language. There is no numeric comparison, no negation, no `contains`, and none of the expression functions you can use in a `condition:`. Anything more expressive has to be encoded into the capability value itself, or handled by splitting the pool. ## The failure mode worth knowing Demands are a *scheduling filter*, not a validation step. If you demand a capability that no agent in the pool reports — a typo, a capability set on a machine that has since been decommissioned, an agent that has not been restarted since the tool was installed — the job is not rejected. It goes to the queue and waits for an agent that will never appear, until the job's wait exceeds its timeout and it fails. So the symptom is a job stuck at "waiting for an available agent", and the diagnosis is to open the agent pool, look at an agent's **Capabilities** tab, and compare it letter by letter with the demand. Capability names and values are matched exactly. A related trap: installing a tool on a self-hosted agent does not update its capabilities until the agent process restarts, so the demand keeps failing on a machine that visibly has the tool. ## Choosing between them Reach for a self-hosted pool with demands when a job needs something a hosted image cannot give: private network access to an internal artifact store or database, specialised hardware, a licensed tool, unusually large disk or memory, or a warm dependency cache that makes builds materially faster. Otherwise the hosted image is almost always the better default, because a fresh machine per job removes an entire category of "works on the build server" problems, and because the machines are somebody else's responsibility to patch. A common middle ground is to run most jobs on `vmImage` and reserve a small self-hosted pool, addressed by demand, for the two or three jobs that genuinely need it.
- A job demands a capability no agent in its pool reports. What does the run look like?It looks hung, not broken. Demands filter the queue rather than validate the YAML, so the job sits waiting for an agent that will never match and eventually fails on timeout. Diagnose it by comparing the demand character-for-character against an agent's Capabilities tab in the pool settings.
- You installed a tool on a self-hosted agent but the demand for it still never matches. Why?System capabilities are discovered when the agent process starts, so a tool installed afterwards is invisible until the agent restarts. Restart the agent service, then check the capability appears in the pool UI. If the tool is not something the agent auto-discovers, add it as a user-defined capability instead.
- Why prefer ubuntu-22.04 over ubuntu-latest for a Microsoft-hosted pool?Because `*-latest` is a moving target: Microsoft rotates which image it points at, and the preinstalled toolchain shifts with it. A pipeline that has not changed can start failing on a Tuesday. Pinning the image makes upgrades an explicit commit you can revert, at the cost of having to schedule those upgrades yourself.
saying these in an interview costs you the question
- Thinks vmImage names a container the steps run inside
- Believes demands support functions like eq() or contains()
- Expects an unsatisfiable demand to fail the job immediately
- Assumes a hosted agent can reach a private network
- Says capabilities update the moment a tool is installed