← The ADLC library
Field guides · 24

Finding the work Azure DevOps never recorded

Azure DevOps has the strongest native lever of any tracker for preventing untracked work, and most teams have it switched off. Here is how to find what already slipped through, and how to stop the next lot.

You are reconciling what shipped against what was planned, and the two lists do not agree. Boards shows a tidy sprint. The repository shows a pile of merges that map to nothing on it. Somebody is going to ask you which one is true, and the answer is that they both are, for different definitions of the work.

First, work out which topology you are in

This matters more in Azure DevOps than anywhere else, because the product spans both halves of the problem and most organisations only use one half of it.

Boards and Repos together. Work items and code live in the same product. Links between them are first-class, the pull request can require a linked work item before it merges, and the join is essentially free.

Boards with GitHub. Very common, and a completely different situation. The link is carried by a mention syntax in commit messages, pull request titles or pull request descriptions, which the Boards integration for GitHub recognises and turns into a link on the work item. It works well and it depends on a human typing something.

Boards with external source control

  • Link depends on a mention typed by a person
  • Enforcement lives in the other product's branch rules
  • Connection health is a thing you have to monitor
  • Orphans accumulate silently

Boards with Azure Repos

  • Work item links are attached to the pull request directly
  • A branch policy can require one before merge
  • The link survives independently of commit message text
  • Orphans are prevented rather than detected
If you are in the left column, most of this guide is detection work. If you are in the right column and still have orphans, you have a policy that is not switched on.

Give it credit before you go hunting. Work items carry typed links, including links to branches, commits and pull requests, which surface in the work item’s development section. Pull requests carry their own history of reviewers, votes, policy evaluations and the completion event. Branch policies can gate completion on required reviewers, successful builds, comment resolution and, importantly here, the presence of a linked work item.

That last one is the single most useful native control in this whole area, and in my experience it is either not enabled or enabled as a warning rather than a requirement. Before you build any detection tooling, go and look at whether it is on for your protected branches. If it is off, turning it on will do more than anything else in this guide.

The limit you will hit

Work item queries in Azure DevOps are excellent at querying work items and at querying links between work items. They are not the tool for finding code that has no work item, for the same reason no tracker can be: there is no record to return. A query language cannot enumerate absences.

So the search runs from the code side, the same way it does everywhere else, and the tracker is used only to resolve what you find.

Querying it, once you know the topology

Enumerate completed pull requests over your window. From Azure Repos, or from GitHub, GitLab or Bitbucket if that is where your code lives. Capture identifier, title, description, source and target branch, author and completion time. The REST interface exposes this and paginates, so write the pagination properly rather than taking the first page.

Enumerate direct pushes to protected branches separately. Commits that landed without a pull request are a distinct population and usually a distinct root cause. They are also where the interesting findings are, because they are almost always incident work.

Resolve links rather than pattern-matching text. If you are in Azure Repos, the pull request itself carries its work item links, which is far more reliable than reading the description. If you are on GitHub with the Boards integration, extract the mention and then confirm through the API that the referenced work item exists, is in a project you care about, and is not a stale identifier from a migrated project. A reference that looks right and resolves to nothing is the most misleading result in this whole exercise, because it appears in your linked column.

Check the connection itself. Cross-product integrations break. A revoked token, a repository renamed, a repository moved to a different organisation, an approval that lapsed: any of these stops new links being created while everything continues to look normal on both sides. A gap in your linking rate that starts on a specific date and never recovers is almost never a culture change. Go and look at what happened that day.

PopulationCompleted pull requests plus direct pushes into protected branches, over a fixed window.
ResolutionReal links where the product carries them; resolved mentions where it does not.
ClassificationNo link, unresolvable link, bypassed the pull request, or work item with no code.
OutputOne row per change. The ratio is a summary of the rows, never the artefact itself.
The "unresolvable link" bucket is the one that surprises people. It reports as linked on every dashboard and contains nothing.

Closing each cause, in order of effort

Turn the branch policy on. If you are in Azure Repos, requiring a linked work item on pull requests targeting protected branches costs nothing, needs no new tooling, and moves the problem from detection to prevention. Do this before anything else on this list.

Reduce the cost of making the link. Most missing links are friction, not defiance. If creating the link means leaving the editor, finding the item and copying an id, some fraction will not happen. A branch naming convention that carries the id, and a pull request template that prompts for it, are free.

Warn, then require. Where enforcement is not available natively, a pipeline step that comments on an unlinked pull request without failing it will move the number substantially. Blocking merges in week one produces a placeholder work item created purely to satisfy the check, which is worse than the original problem because it is invisible.

Give urgent work a fast path, not a prohibition. If the out-of-hours process demands a work item before the fix, it will be ignored, correctly. Make the after-the-fact work item trivial to create with the change already attached.

Then consider tooling. If you want this maintained continuously rather than swept quarterly, that is what this category of product does. Ours reads GitHub, GitLab and Bitbucket and keeps the join against the tracker current, with full two-way sync for Azure DevOps, Jira and Linear. The sweep above is genuinely enough for a periodic audit, and if that is all you need, run it and do not buy anything.

Where this breaks down

Pipelines can deploy things that no pull request produced. A release that promotes an artefact built months ago, a manual pipeline run with overridden variables, a rollback to a previous version: all change production, none of them is a merge. If your question is really “what changed in production”, the pull request population is the wrong denominator and you want the deployment record instead.

A required work item link is easily satisfied without being satisfied. The policy checks that a link exists. It cannot check that the work item describes this change, that it was written before the code, or that it says anything at all. Enabling the policy converts an unmeasured problem into a measured one, which is progress, and it does not make the board true.

Classic and Agile process templates behave differently. Work item types, states and link types vary by process template, and organisations that have migrated between them carry both. Any script here needs to be written against your project’s actual configuration rather than the defaults in a tutorial, and any cross-project comparison needs to say which templates it is mixing.

Cross-product integration health is a permanent maintenance item. In the Boards-with-GitHub topology, the link depends on a connection that somebody owns and that will break. If you build reporting on it, build an alert on the connection alongside it, because a linking rate that quietly reads zero is indistinguishable from a team that quietly stopped caring.