Loops in Azure DevOps Pipelines

Janne Kemppainen |

If you need to do the same thing over and over again, why repeat yourself? In this post we’ll discover how looping works in Azure DevOps pipelines.

The basics

The Azure DevOps documentation briefly mentions the each keyword with a sample usage:

- name: listOfStrings
  type: object
  - one
  - two

- ${{ each value in parameters.listOfStrings }}:
  - script: echo ${{ value }}

The each keyword works the same way you’d expect from your typical programming language. It goes over an iterable one item at a time and stores the value in a variable of your choice, in this example: value.

The loop needs to follow the YAML syntax. Since steps is a list of items, the each statement must also start with a hyphen -. The statement itself is contained inside ${{ }} and the line ends with a colon character. Everything that you want to be included in the loop needs to be indented one level further than you’d normally do.

There are many parameter data types in Azure DevOps, but there is no separate type for a list of strings. In this case we need to use the object type which accepts any YAML structure. The default value is used when no values are provided.

It is important to understand that the loop is evaluated when the pipeline is parsed, so it generates all steps in advance. You can’t change them during the pipeline runtime. That’s why the parameter values need to be defined before the pipeline execution starts.

You can only use parameters in each loops since variables in Azure DevOps pipelines are always strings. Parameters that are defined at the top level of the pipeline can be changed at startup. Template parameters need to be passed when calling the template.

Looping a map

Maps can also be looped, so you don’t need to use lists for everything. Each item in a map contains a key and a value that you need to reference instead of the object itself.

The parameters object itself is also a map, and can be looped through:

- name: environment
  type: string
  default: test
  - production
  - staging
  - test
- name: runTests
  type: boolean
  default: true

- ${{ each parameter in parameters }}:
  - script: echo "${{ parameter.value }}"
    displayName: Print ${{ parameter.key }}

This pipeline creates a step for each parameter and uses echo to print the value to the console. See how the key and value need to be accessed.


Handling objects in loops can cause surprising effects. Since objects don’t have automatic type conversion to a string in Azure DevOps, you will see this error if you try to output an object:

/azure-pipelines.yml (Line: 19, Col: 13): Unable to convert from Object to String. Value: Object

Unfortunately, Azure DevOps doesn’t seem to have a way to infer the variable type so mixing strings and objects in the same loop can be a bit tedious.

Example: Authenticate multiple .npmrc files

Let’s take a look at a practical example.

The npm Authenticate task works on the local .npmrc file from the repository to fetch and store credentials that are used for private registry authentication. Unlike your typical developer authentication methods, npmAuthenticate doesn’t edit the global configuration file in the user’s home directory, but it adds the credentials directly to the file inside the repository instead.

In a monorepo structure each component may have its own .npmrc file. In this case it’s not enough to only authenticate the repository top level file.

We can solve the issue by using the each loop in a template:

  - name: npmrcFiles
    type: object
    default: [".npmrc"]

  - ${{ each workingFile in parameters.npmrcFiles }}:
    - task: [email protected]
        workingFile: ${{ workingFile }}
      displayName: Authenticate ${{ workingFile }}

It goes through each item in the npmrcFiles parameter and calls the [email protected] task that many times. If the parameter is omitted it defaults to .npmrc at the root of your project.

If you’ve stored the template as templates/authenticateNpm.yaml, the main pipeline could look something like this:

  vmImage: "ubuntu-latest"

  - template: templates/authenticateNpm.yaml
      - contact-form/.npmrc
      - store/.npmrc
      - user-api/.npmrc
  - bash: cd contact-form && npm ci && npm test
  - bash: cd store && npm ci && npm test
    condition: succeededOrFailed()
  - bash: cd user-api && npm ci && npm test
    condition: succeededOrFailed()

The example pipeline authenticates three components, installs their dependencies and runs the unit tests.

The test commands look repetitive. How would you reorganize the pipeline to utilize looping?

Subscribe to my newsletter

What’s new with PäksTech? Subscribe to receive occasional emails where I will sum up stuff that has happened at the blog and what may be coming next.

powered by TinyLetter | Privacy Policy