Linking Azure DevOps work to code, reliably
Azure DevOps can refuse to complete a pull request that has no work item attached. Here is how the linking actually works, when to turn that policy on, and what it still will not catch.
Somebody upstairs wants to know which work items shipped in the last release. You open the release, you get a list of commits, and turning that list into a list of work items takes an afternoon and produces a number you do not trust. Meanwhile half your boards show items closed with no code attached at all.
Azure DevOps is better placed to solve this than almost anything else, for one reason: it can refuse to let a pull request complete unless a work item is linked. Most tools can report on the gap. Azure DevOps can close it. Whether you should turn that on is a real decision, so let us take the mechanism apart first.
First-class links, not text matches
Work items and repositories live in the same product, and the links between them are first-class objects rather than text matches. A commit, a branch, a pull request and a build can each be attached to a work item as a typed relationship, and the work item shows all of them.
Those links get created four ways:
Typed into a commit message or pull request description. A # followed by the work item ID creates the link. The ID is a bare number with no project prefix, which matters more than it looks and we will come back to it.
Manually, from either side. Attach a pull request to a work item, or a work item to a pull request, in the interface. Reliable, and dependent on somebody remembering.
Created from the work item. Start a branch or a pull request from the work item itself and the link exists before anyone types anything. This is the highest-compliance route and the least used.
From GitHub, when your code is not in Azure Repos. The Azure Boards integration for GitHub reads AB# followed by the work item ID out of commit messages, pull request titles and pull request descriptions. The AB# prefix exists precisely because a bare #123 in a GitHub repository already means a GitHub issue or pull request, so the two namespaces had to be separated.
Then the part that makes this product different: branch policies. A policy on a branch can require that a pull request has at least one linked work item before it can complete, and it can be set to blocking or merely advisory. There are companion policies for build validation, reviewer counts and comment resolution. Where exactly these sit in the settings has moved between versions, so find them under the branch’s policy configuration in your own instance rather than following a path from memory.
Finally, reporting. Azure DevOps exposes an analytics layer you can query for work item state and its related links, which is how you build a linking-rate report that refreshes rather than an afternoon of clicking. The query surface and its available entities differ between Services and Server, so check what your deployment actually exposes before designing a dashboard on it.
A bare work item ID carries no context
Work item IDs carry no context. Unlike a tracker that stamps a project prefix onto every key, an Azure DevOps work item ID is a bare number. A branch called feature/4471 tells you nothing about which team, which project or even whether 4471 is a work item at all, since pull request IDs and build IDs are also bare numbers in the same range. Humans reading history cannot disambiguate, and neither can a naive script.
The policy is per branch and per repository. Turn it on for main in one repository and it is on in exactly one place. Nothing propagates it, and a repository created next month arrives with none of it. This is the most common way an organisation believes it has enforcement and does not.
Squash completion rewrites the message. If the reference lived only in individual commits and the pull request completes as a squash, the trunk commit carries the generated message. If the reference is on the pull request, it survives. If it was only in commits, it may not.
The policy accepts any work item. Linking a stale item, a parent epic or someone else’s task satisfies the check completely. The gate proves a link exists, not that it is the right one.
Automated pull requests hit the wall. Dependency bots and generated updates have no work item, so a blocking policy either stops them dead or forces an exemption path, and the exemption path is where discipline leaks out.
Cross-boundary work. A change serving two projects gets attached to one item, and the other project’s board never sees the code.
Get your current number before you change any policy
Get your current number before you change any policy. You can pull merged pull requests and their linked work items through the Azure DevOps REST API, or through the analytics layer if your deployment exposes it. Do it per repository, and exclude your service accounts from the denominator. A team that turns on a blocking policy without knowing whether it currently sits at 85 percent or 40 percent is choosing between a formality and a riot without knowing which.
Then, in order of cost:
- Make starting from the work item the normal route. Creating the branch from the item removes the entire failure mode rather than policing it. Cheapest intervention available and it needs no configuration at all.
- Turn the linked-work-item policy on as advisory first. Advisory gives you a visible, per-pull-request signal without blocking anybody, which is the honest way to find out what your real compliance is and to let people complain before it bites.
- Then make it blocking on your protected branches, with a named exemption path. Decide in advance which service accounts are exempt and how a genuine no-item change gets through, and write that down. If you do not, someone will create a junk work item, and junk work items are worse than no policy because they poison the reporting you built the policy for.
- Apply it as configuration, not by hand. Branch policies can be set through the API and through infrastructure tooling. If new repositories get created regularly, provisioning the policy alongside the repository is what stops the estate drifting apart.
For a single project with a stable set of repositories, that is the complete answer and you should stop reading here.
Where this breaks down
Blocking policies buy compliance, not traceability. The moment completion depends on a link, the incentive is to produce a link, and the cheapest link is a work item created moments earlier with a one-word title. You have converted an honest gap into a dishonest record. Watch the age distribution of linked items after you turn the policy on: a spike of items created within minutes of their pull request is the tell.
Advisory policies get ignored. The intermediate step I just recommended has a real weakness, which is that a non-blocking warning is a warning nobody reads after week two. It is a measurement device, not a solution, and treating it as the end state is a way to feel busy.
Per-repository configuration does not scale by hand. At twenty repositories this is tedious. At two hundred it is not done. If you cannot automate the policy, do not pretend the estate is governed.
The bare-ID problem stays. Nothing in Azure DevOps makes 4471 self-describing in a branch name. Conventions can add a prefix by agreement, but git history will always contain the years of branches created before you agreed.
The takeaway
Azure DevOps is one of the few environments where you can make the link mandatory rather than merely measured, and that is a genuine advantage. It is also a loaded gun, because a mandatory link produces a link every time, including when there is nothing real behind it.
If you take one thing into next week: turn on the linked-work-item policy in advisory mode on one busy repository and look at the warning rate for a fortnight. That number, not anyone’s opinion, decides whether you are ready to make it blocking.