---
title: Azure DevOps
description: Monitoring of Azure Pipelines (YAML and classic) in Pipedash on dev.azure.com or Azure DevOps Server, with branch and variable triggers, run cancel and live job logs.
---

The Azure DevOps plugin (`azure-devops`) monitors the build pipelines of the selected Azure DevOps projects. It shows the latest build of each pipeline, loads run history, queues builds, cancels running builds and shows job logs inside Pipedash. It lists YAML and classic pipelines.

## Requirements

The plugin authenticates with a personal access token (PAT). It has no sign-in flow and no other login method. Pipedash sends the token over HTTP Basic authentication with an empty user name.

The token needs these scopes:

| Scope | Required | What it unlocks |
|---|---|---|
| Build (Read & execute) | Yes | Pipelines, builds, timelines, logs, queueing a build and canceling a build |
| Project and Team (Read) | Yes | Listing the projects of the organization |

Pipedash confirms the token with a request that lists one project. It does not test the scopes one by one.

A `401` answer is an authentication error and a `403` answer is a permission error. Azure DevOps also answers an invalid or expired token with a sign-in page, a redirect or a `203` status. Pipedash reports these as an authentication error that ends with "Check that the personal access token is valid and has not expired".

## Configuration

<Screenshot src="provider-azure-devops" variant="dialog" alt="Add provider dialog with Azure DevOps selected, showing the Organization URL and Personal Access Token fields" caption="Azure DevOps asks for the organization URL and a personal access token." />

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `server_url` | Text | Yes | None | URL of the organization, for example `https://dev.azure.com/my-org`, or of an Azure DevOps Server collection, for example `https://tfs.example.com/DefaultCollection`. The form labels it "Organization URL". It must match `^https?://[^\s?#]+$`, and the form says "Must be a valid HTTP or HTTPS URL" otherwise. |
| `token` | Password | Yes | None | Azure DevOps personal access token with the scopes Build (Read & execute) and Project and Team (Read). The form labels it "Personal Access Token". |
| `status_page_url` | Text | No | None | Statuspage address for the instance. See the status page section. The form labels it "Status page URL (optional)". |

The config table of the TOML file also takes `selected_items`, a comma-separated list of project names, for example `Web App,Platform`. Spaces around a comma are ignored, and spaces inside a name stay. The picker writes these values.

## Base URL

There is no `base_url` key. `server_url` is the base URL and has no default. A missing or blank value fails with "Missing Azure DevOps server URL". A value that does not parse as a URL fails with "Invalid Azure DevOps server URL" and the parse reason. A URL with another scheme than `http` or `https`, or without a host, fails with "Azure DevOps server URL must start with http:// or https://".

Pipedash trims the value and removes a query and a fragment. It ignores a trailing slash. It builds every request from this URL:

| Request | Address |
|---|---|
| Organization level | `<server_url>/_apis/...` |
| Project level | `<server_url>/<project>/_apis/...` |

Every request carries `api-version=7.0`. The plugin has no setting for another version. It has no option to skip certificate checks either.

The organization name in the picker and in the repository column is the last non-empty path segment of `server_url`, or the host name when the URL has no path. `https://dev.azure.com/my-org` gives `my-org`. `https://tfs.example.com/tfs/DefaultCollection/` gives `DefaultCollection`.

## Pipeline discovery

Discovery asks for an organization first. The list holds one entry, the organization name from `server_url`. The picker then lists the projects of the organization, sorted by name without regard to case. Each entry shows the project description when it has one. Search matches the project name and the description and ignores case.

The selection holds projects, not single pipelines. `selected_items` is the list of project names. Pipedash shows every build definition of each selected project, one pipeline per definition, named like the definition. The repository column shows `<organization>/<project>`.

For each pipeline, Pipedash shows the latest build of the definition. The status, the branch and the last run time come from that build. The last run time is the start time of the build, or its queue time when the build has not started. A definition without a build shows `pending`.

Pipedash fetches the selected projects at the same time. A failure on one project fails the whole refresh. An empty `selected_items` shows the error `No projects configured`.

Azure DevOps splits long lists into pages. Pipedash reads pages of 100 items and follows the continuation token of each answer for up to 100 pages. A list with more items than that fails with the error `Azure DevOps list <path> still has more items after 100 pages`.

Saving a provider in the app or through the REST API drops selected projects that have no build definition. A project whose check fails stays. If every selected project has no definition, the save fails with "None of the selected repositories has a CI file or a pipeline run yet". Providers that come from the TOML file or an import keep their `selected_items` as written.

## Run history

Pipedash lists the builds of one definition, newest queue time first. It reads pages of up to 100 builds. It stops at the number of runs the history view needs, or after 100 pages.

<Screenshot src="provider-azure-devops-history" alt="Run history of the payments-api Azure DevOps pipeline with the Run and Build columns, statuses and branches" caption="An Azure DevOps run shows the build id as Run and the build number as Build." />

The run number is the build ID. The build number, such as `20261028.1`, shows in the "Build" column. Each run has these values:

- The branch is the source branch without the `refs/heads/` prefix. Other refs, such as `refs/pull/17/merge`, stay whole.
- The commit is the source version of the build. Azure DevOps builds carry no commit message in Pipedash.
- The actor is the display name of the user the build was requested for.
- The start time is the start time of the build, or its queue time when the build has not started. The duration needs both a start and a finish time.
- The run link opens the build results page in Azure DevOps.
- The inputs hold the branch as `source-branch` and the variables the build was queued with.

Run details load one build by its ID. When the build belongs to another definition, Pipedash returns the error `Run #<id> does not belong to <pipeline ID>`.

## Trigger parameters

Pipedash reads the definition and offers one field for the branch and one field for each pipeline variable that a run may override.

| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| `source-branch` | String | No | Default branch of the repository of the definition | Branch to build. The form labels it "Branch". An empty value uses the default branch of the definition. |
| One per variable | String | No | Value of the variable | A pipeline variable that allows override at queue time and is not secret. The form labels it with the variable name. Variables appear in alphabetical order after the branch. |

Pipedash queues the build with `POST _apis/build/builds` and a body that holds the definition ID. It adds two more entries when they apply:

- `sourceBranch` is the branch with `refs/heads/` in front. A value that already starts with `refs/` goes as given.
- `parameters` is a JSON object, sent as a string, with the variables to override.

Pipedash drops an input that is empty after trimming. It sends a boolean or a number as text. It skips other value types. Pipedash sends only the inputs whose name matches an overridable, non-secret variable of the definition, ignoring case. It leaves out all other inputs, so the REST API cannot set a variable the definition does not allow to override.

The trigger answer reads `Queued build <id> on <branch>`. When no branch was given, it reads `Queued build <id> on the default branch`.

## Cancel

Pipedash cancels a run with `PATCH _apis/build/builds/{id}` and the body `{"status": "cancelling"}`. This needs the Build (Read & execute) scope. Before it sends the request, Pipedash loads the build. When the build belongs to another definition, it returns the error `Run #<id> does not belong to <pipeline ID>` and sends no cancel request.

Azure DevOps reports a build in cancellation as `cancelling`. Pipedash shows it as `running` until the build completes. The button is unavailable for a `pending` run.

## Rerun

The plugin has no rerun call. It has no "Re-run failed jobs" action either. A rerun queues a new build through the same request as a trigger, with the inputs of the old run: the branch and the variables the build was queued with.

In the run history, Rerun opens the trigger form with these values filled in. In the run details, Rerun asks for confirmation and then queues the build. Pipedash sends only the variables that the definition still allows to override.

## Run logs

The Logs tab of the run details shows the timeline of the build as a tree:

- A stage is a top-level node. A build without stages lists its jobs at the top.
- A job sits below its stage. It carries the log. Pipedash shows its state and result with the same mapping as the status mapping section below.
- A step sits below its job. It has no log of its own. The log of the job holds the output of all its steps.

Pipedash sorts the nodes by the order that Azure DevOps reports and puts nodes without an order last. It leaves out timeline records of other types.

Pipedash reads the timeline with `GET _apis/build/builds/{id}/timeline`. For a job it finds the log ID and the attempt in the timeline. It then downloads the log as plain text with `GET _apis/build/builds/{id}/logs/{logId}` and streams it to disk. A download stops after 15 minutes.

A job shows its log as follows:

| Job state | Logs tab |
|---|---|
| Not started | "Logs appear when this job finishes" with its steps. The log opens by itself once Azure DevOps lists a log for the job. |
| Running, no log yet | The same message. |
| Running, with a log | The log so far. It updates while the job runs. |
| Finished, with a log | The whole log. |
| Finished, without a log | No log. A job that was skipped or canceled while it waited is an example. |

While a run has not finished and the Logs tab is open, Pipedash refreshes the tree and the run status every 5 seconds. It reads the log of an open running job again about every 2 seconds and appends only the new lines. For this it keeps the number of lines it has seen, a hash of the last line and the attempt. It asks Azure DevOps for the log from the last line it has seen with `startLine`. It checks that the first line it gets back is that last line. When the last line it has seen had no line break, the new text starts on a fresh line. Pipedash downloads the whole log again and replaces the shown log when it cannot verify the position. This happens when:

- The cursor belongs to another build, log or attempt.
- The last line is longer than 1 MiB.
- Azure DevOps returns a line that does not match.

After a failed log read, Pipedash waits 5 minutes before it tries again. When the tree refresh fails, it waits longer each time, up to 60 seconds.

Errors:

- When Azure DevOps answers `404` or `410`, the error reads `Log <logId> of build <buildId> is no longer available`.
- When the timeline lists no log for the job, the error reads `Run #<id> has no job log <node ID>`.

The log follows the Azure Pipelines line format. Each line can start with a timestamp, and Pipedash reads it as the time of the line. Pipedash reads these markers:

- `##[section]Starting: <task>` starts a step and a collapsed section titled with the text after the marker.
- `##[section]Finishing: <task>` stays a plain line inside its section.
- Any other `##[section]` line, for example `##[section]Async Command Start: <name>`, starts a collapsed section but not a step.
- `##[group]` and `##[endgroup]` open and close nested sections.
- `##[error]`, `##[warning]`, `##[command]` and `##[debug]` lines get their own style. A task output line that starts with `[command]` and no hashes counts as a command.

Only `##[error]` lines count as errors. Pipedash does not scan plain output for error words. A `##[section]` marker in the middle of a line is plain output. The [Run logs](/docs/features/run-logs) page covers the viewer.

## Status page

Azure DevOps has no default status page. `status_page_url` points Pipedash at the Statuspage of the instance. It follows the rules of the [GitHub Actions status page](/docs/providers/github-actions#status-page). An invalid value makes the provider fail to initialize. See also [Provider health](/docs/features/provider-health).

## TOML example

```toml
[providers.azure-work]
name = "Azure DevOps Work"
type = "azure-devops"
token = "${AZURE_DEVOPS_TOKEN}"
refresh_interval = 30

[providers.azure-work.config]
server_url = "https://dev.azure.com/my-org"
selected_items = "Web App,Platform"
```

For Azure DevOps Server, set `server_url` to the collection URL, for example `https://tfs.example.com/DefaultCollection`.

## Status mapping

Pipedash maps the build `status` and, for completed builds, the `result`. Timeline records of stages and jobs use the same mapping with their `state` and `result`.

| Pipedash status | Azure DevOps state |
|---|---|
| `success` | `completed` with result `succeeded` |
| `failed` | `completed` with result `failed`, `partiallySucceeded` or `succeededWithIssues`, or with any other or missing result |
| `running` | `inProgress` or `cancelling` |
| `pending` | `notStarted`, `postponed`, `none`, `all`, a missing status and any other status. A pipeline with no build. |
| `cancelled` | `completed` with result `canceled` or `abandoned` |
| `skipped` | `completed` with result `skipped` |

This plugin does not produce the `degraded` status.
