Azure DevOps Work Item Tags
1. Overview
A work item tag in the Event Membership project is one of four facets, written facet:value. An unprefixed tag is not part of the vocabulary and is removed on sight.
The rule exists because the alternative was the default, and the default failed in a specific and measurable way. Left ungoverned, the project accumulated 130 distinct tags spread across 128 tagged work items — a vocabulary as large as the corpus it described. Eighty-six of those tags were used exactly once. A tag used once cannot be queried, cannot be trusted to be complete when it is queried, and is indistinguishable from a word already present in the title. A third of all tag applications restated a field the work item already carried.
The failure mode is not noise for its own sake. It is that a tag which looks like an index is not one. Searching data-integrity returned seven items while data-quality held three more and data-loss a fourth, and nothing in the tag picker revealed that the other two spellings existed. The reader who finds a plausible tag stops looking, and concludes they have the full set.
GitHub Labels governs the equivalent vocabulary on pull requests. The two are deliberately different shapes, for reasons that page gives.
2. The two tests
A candidate tag must pass both. Most candidates fail the first.
2.1. The redundancy test
If a field already answers it, it is not a tag.
The backlog is partitioned on three axes, and they are held apart deliberately. Collapsing them is what produced the calendar-bin backlog they replaced.
| Axis | Question it answers | Mechanism |
|---|---|---|
Component |
Which codebase is edited? |
Area Path, |
Stream |
Which durable initiative owns it? |
Epic parent link |
Goal |
Which dated outcome needs it? |
Tag |
Beyond those three, the work item type carries the kind of work, Priority carries urgency, Iteration Path carries when, State carries whether it is live, and the native branch and pull request links carry the code. None of these is ever restated as a tag. A tag that duplicates a field is worse than no tag, because the two drift and the reader cannot tell which is current.
2.2. The query test
If nobody will write a query on it, it is not a tag — it is a word in the title.
The purpose of a tag is to make a set retrievable that no field can express. risk:security is retrievable and the set is meaningful. A tag naming the technology involved — the framework, the library, the error code, the entity — is not retrieved, because anyone looking for it searches the title, where the word already is. Tagging it adds a maintenance obligation and returns nothing.
The practical form of this test: before adding a tag, write the WIQL query you intend to run on it. If the query is one you would not actually run, or one that Title CONTAINS answers just as well, do not add the tag.
3. The facet rule
Every tag is facet:value, lowercase, hyphen-separated within the value.
The prefix is load-bearing, and not merely cosmetic:
-
It makes an illegal tag visible on sight. A tag with no colon is out of vocabulary, and that can be checked without knowing the vocabulary.
-
It makes the facet sweepable.
[System.Tags] CONTAINS 'goal:'returns every dated-outcome tag at once. Unprefixed, there is no way to enumerate a facet without already knowing every value in it. -
It groups the picker. Azure DevOps sorts tags alphabetically in the tag input, so a prefix clusters a facet’s values together and shows the author what already exists before they invent a synonym.
-
It gives governance a gradient. Adding a value to an open facet is free. Adding a facet requires this page to change. Without the prefix there is no distinction between the two, and every new tag is an unreviewed extension of the taxonomy.
4. The four facets
| Facet | Vocabulary | Answers | Retirement |
|---|---|---|---|
|
Open |
Which dated outcome needs this? |
When the goal is met |
|
Open |
What came out of this incident? |
When the root cause analysis closes |
|
Closed |
What property spans every component and must be sweepable? |
Never — it is a property, not a state |
|
Closed |
Who found it, and does that change how we answer? |
Never |
4.1. goal:<slug>
A dated outcome that several work items serve and that completes. goal:tdw-2026 is the worked example.
Open vocabulary: a new goal is created by tagging the first work item, no change to this page. The Control Tower is the register of live goals and is where a goal’s meaning is recorded.
Streams are durable and never finish; goals are dated and complete. This is the distinction that decides whether something is a goal tag or an Epic. An event is a goal. Modelling an event as a stream strands its work in a dead bucket the day after the race.
An item may carry several goal tags. It carries exactly one stream, which is not a tag.
Retire a goal tag when the outcome is met: the work items keep their history through the Epic and the iteration, and a met goal that stays in the picker is offered to the next author as though it were live.
4.2. inc:<YYYYMMDD>
The incident a work item came out of, dated by the day the incident opened — inc:20260917.
Open vocabulary, and the date makes collision impossible without any coordination, which is the property that matters when the tag is created under pressure. A second incident on the same day takes a suffix: inc:20260917b.
This facet exists because the question "what did that incident cost us, and is all of it closed?" has no other answer. The remedial work spreads across components and streams by its nature — that is what an incident does — so neither Area Path nor Epic gathers it. It is the one genuinely irreducible grouping in the taxonomy.
Scope it to remedial work that would not otherwise exist: the fix, the guard, the monitoring gap, the follow-up hardening. Not every item touched while diagnosing.
Retire the tag when the root cause analysis closes. The incident narrative itself lives in the design journal or a runbook, not in the tag and not in a design document.
4.3. risk:<kind>
A property of the work that spans components and must be sweepable as a whole.
Closed vocabulary. These five, and no others without a change to this page:
| Value | Applies when |
|---|---|
|
Authentication, authorisation, credential handling, injection, exposure, or a dependency advisory |
|
Data can be destroyed or become unrecoverable |
|
Data can become wrong, inconsistent, or silently diverge between systems |
|
A latency, throughput, or resource-exhaustion property is at stake |
|
A regulatory, contractual, or audit obligation is engaged |
risk:data-loss and risk:data-integrity are distinct and the line is worth holding: integrity is wrong data, loss is no data. An item may carry both.
The facet is closed because its value is entirely in exhaustiveness. A security sweep that misses a third of the set because someone wrote authentication instead is not a sweep — it is a false clean bill of health, which is worse than no sweep. Five values can be held in the head; a sixth invites a seventh.
4.4. src:<origin>
Where the work item came from, kept only where the origin changes how the item is answered.
Closed vocabulary:
| Value | Applies when |
|---|---|
|
A customer reported it. Someone outside the team is waiting and expects an answer |
|
It came out of a production incident. Pairs with an |
|
An automated scanner raised it — dependency advisory, static analysis, security scan |
|
A deliberate review found it — a code review sweep, a data audit, a design review |
Provenance is a tag rather than a field because it changes the answer, not the classification. A defect reported by a customer is answered even when it is minor, because someone is waiting. A defect from a scanner is triaged against exploitability before it is worth anyone’s afternoon. Priority cannot express that difference: both may sit at P3 and want opposite handling.
Most work items carry no src: tag. The absence means the team found it in the ordinary course of work, which needs no marking.
5. What is not a tag
Each of these was in use before the vocabulary was closed. The table records where the fact belongs instead, because the question recurs.
| Was tagged as | Belongs in |
|---|---|
The component — a service, portal, library or repository name |
Area Path. The tree already carries every one of them. If the right node does not exist, add the node |
A priority — |
Priority field. It is set on every work item and it sorts; a tag does neither |
The work item type — |
Type. It is already in every query result and every board column |
A parent — |
The parent link. A tag naming a parent goes stale the moment the item is reparented, and silently |
A branch or pull request |
The native branch and pull request links. These resolve, show state, and are created by the tooling |
A phase, stage or sprint — |
Iteration Path |
Deferral — |
State and Iteration Path. A tag saying "later" cannot be sorted by date and is never swept |
An environment — |
Nothing. Every production defect would carry it, so it partitions nothing. Where an item is genuinely stage-only or dev-only, that belongs in the description |
A technology — the framework, library, error code, entity or endpoint |
The title. It is searched there, and the tag adds an obligation without adding reach |
A feature abbreviation — a three-letter code for a capability |
The title, which in every observed case already spelled it out in full |
6. Extending the vocabulary
Adding a value to an open facet — a new goal: or inc: — needs nothing. Tag the work item.
Adding a value to a closed facet — a sixth risk:, a fifth src: — changes this page, in the same change as the first work item that carries it. The test to apply is the query test in its strictest form: state the sweep the value makes possible, and state why the existing values do not already cover it.
Adding a facet changes this page and the tag-selection skill. A new facet is a new axis, and four axes already cost more to keep straight than most teams manage. The bar is that the facet answers a question no field and no existing facet answers, and that the answer is one somebody actually needs.
Removing a tag from a work item needs no ceremony. A tag outside the vocabulary is stripped when found.
Removing it from the project needs a second step. Stripping a tag from its last work item does not remove it from the project’s tag registry, and it continues to be offered in the tag picker — which is how a retired tag gets applied again. Delete it explicitly:
TOKEN=$(az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 \
--query accessToken -o tsv)
BASE=https://dev.azure.com/idealogic-za/Event%20Membership/_apis/wit/tags
# List the registry; anything without a facet prefix is out of vocabulary
curl -s -H "Authorization: Bearer $TOKEN" "$BASE?api-version=7.1-preview.1"
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$BASE/<tagId>?api-version=7.1-preview.1"
The delete is eventually consistent — the list endpoint may keep returning a deleted tag for a few seconds.
7. Querying
Sweep a facet:
SELECT [System.Id], [System.Title], [System.State]
FROM WorkItems
WHERE [System.TeamProject] = 'Event Membership'
AND [System.Tags] CONTAINS 'risk:security'
AND [System.State] IN ('New', 'Active')
CONTAINS on System.Tags is a substring match, not a set membership test. Two consequences:
-
CONTAINS 'goal:'enumerates the whole facet. This is the property the prefix buys. -
A value that is a prefix of another value matches both. Keep values distinct rather than nesting them — this is why
inc:uses a date rather than an incrementing number.
A tag write replaces the entire tag string. Read System.Tags, append, write back. A blind write of one tag silently discards every other tag on the item.
op: add cannot remove a tag — it merges. This is the trap, because it fails silently and convincingly. A JSON-patch add on /fields/System.Tags unions the supplied tags with the ones already there: supplying a shorter list adds nothing and removes nothing, yet the call returns 200 and bumps the work item revision, so both the response and the revision history look like a successful edit. Use replace for any write that drops a tag:
[{"op": "replace", "path": "/fields/System.Tags", "value": "risk:security; goal:tdw-2026"}]
add remains correct when only adding. replace is correct in both cases, so prefer it always.
8. Related
-
GitHub Labels — the pull request vocabulary, and why it is a different shape
-
Development Workflow — where work items sit in the delivery cycle