ado - Azure DevOps CLI
Use az devops (plus az boards, az repos, az pipelines) from PowerShell. Assumes az login and org/project defaults.
Configuration
If ado\config.json is missing, ask:
- •What is your ADO organization URL? (e.g.,
https://dev.azure.com/myorg) - •What is your project name?
- •What area path should Epics use?
- •What area path should Features/Stories/Tasks/Bugs use?
- •What iteration path should work items use?
Save to ado\config.json (gitignored). Config shape:
{
"organization": "https://dev.azure.com/ORG",
"project": "PROJECT",
"areaPaths": {
"epic": "Project\\Team",
"feature": "Project\\Team\\SubTeam",
"story": "Project\\Team\\SubTeam",
"task": "Project\\Team\\SubTeam",
"bug": "Project\\Team\\SubTeam"
},
"iterationPath": "Project\\Iteration",
"storyPointScale": [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
}
Load config:
$config = Get-Content ".\ado\config.json" | ConvertFrom-Json
Prerequisites
Verify auth and defaults:
az account show --query "{name:name, user:user.name}" -o table
az devops configure --list
If defaults are missing:
az devops configure --defaults organization=$config.organization project=$config.project
Rules
- •Always use PowerShell for scripting and JSON; do not pipe to python/node.
- •Use
--queryJMESPath onazcommands to filter JSON before PowerShell when possible. - •For any multi-step operation, read the matching section below first; never guess
azflags or parameters.
State Transitions
User Story lifecycle
New -> Ready to Review -> Ready to Code -> Active -> Closed
| State | Meaning |
|---|---|
| New | Just created, not triaged |
| Ready to Review | Discuss in planning |
| Ready to Code | Planned and estimated |
| Active | In progress |
| Closed | Done |
| Removed | Deleted/cancelled |
Feature and Epic lifecycle
New -> Active -> Closed
Transition commands
az boards work-item update --id ID --state "Ready to Review" az boards work-item update --id ID --state "Ready to Code" az boards work-item update --id ID --state Active az boards work-item update --id ID --state Closed
Quick Reference
Essential commands. For multi-step workflows, read the matching section in "Section Router" first.
# Show a work item
az boards work-item show --id ID --output table
# Show with relations (children, parent, related)
az boards work-item show --id ID --expand relations --output json
# Get child IDs from a parent (e.g., features under an epic)
$json = az boards work-item show --id PARENT_ID --expand relations --output json | ConvertFrom-Json
$childIds = $json.relations | Where-Object { $_.attributes.name -eq 'Child' } | ForEach-Object { $_.url -replace '.*/', '' }
# Show multiple work items (loop - there is no --ids flag)
$childIds | ForEach-Object { az boards work-item show --id $_ --output json } | ForEach-Object { $_ | ConvertFrom-Json } | Select-Object id, @{N='Type';E={$_.fields.'System.WorkItemType'}}, @{N='Title';E={$_.fields.'System.Title'}}, @{N='State';E={$_.fields.'System.State'}} | Format-Table
# Query work items with WIQL
az boards query --wiql "SELECT [System.Id], [System.Title], [System.State] FROM WorkItems WHERE [System.WorkItemType] = 'User Story' AND [System.State] = 'Active'" --output table
# Create a work item
az boards work-item create --type "User Story" --title "Title" --area $config.areaPaths.story --iteration $config.iterationPath
# Link child to parent (no --parent flag exists)
az boards work-item relation add --id CHILD_ID --relation-type parent --target-id PARENT_ID
# Update state
az boards work-item update --id ID --state Active
# Create a PR linked to work item
az repos pr create --title "feat: description" --work-items ID --auto-complete true --delete-source-branch true
# List active PRs
az repos pr list --status active --output table
# List pipeline runs
az pipelines runs list --top 5 --output table
Section Router
| User intent | Section | Read before |
|---|---|---|
| Pick up work items, create branches, make PRs, monitor builds | Dev Flow | Any PR or branch workflow |
| Create features/stories, hierarchy, area paths, estimation | Planning Flow | Creating or linking work items |
| WIP, aging, throughput, cycle time, Kanban health | Backlog Management | Any backlog query or metric |
| Pipeline status, failed builds, logs, triggers | Pipeline Debugging | Any pipeline operation |
| WIQL field names, operators, macros, quoting | WIQL Reference | Writing any WIQL query |
| CLI command patterns, bulk ops, REST API, output formatting | Command Cookbook | Bulk operations or REST API calls |
| Board columns, WIP limits, Kanban column queries | Board Columns API | Any board column query |
Troubleshooting
| Problem | Fix |
|---|---|
az account show fails | Re-run az login - token expired |
| Empty query results | Check area path spelling; for FROM WorkItemLinks use REST API instead |
charmap encoding error | Use az devops invoke not az rest; set $env:PYTHONIOENCODING = "utf-8" |
| Defaults not set | Run az devops configure --defaults organization=$config.organization project=$config.project |
| Permission denied on work items | Verify area path permissions in ADO project settings |
| WIQL syntax error | Check quoting; see WIQL Reference |
Additional Tips
- •Use
--output tablefor readable output,--output jsonfor parsing - •Use
--querywith JMESPath to filter JSON:--query "[].{Id:id, Title:fields.\"System.Title\"}" - •WIQL supports
@Me,@Today,@Today - Nmacros - •Link PRs to work items with
AB#IDin commits or PR descriptions - •Use
az devops wikifor wiki operations - •Use
az boards iterationandaz boards areato manage team structure
Dev Flow - Work Item -> Branch -> PR -> Merge
Use for picking up work items, creating branches, making PRs, or monitoring PR pipelines.
1. Pick up a work item
# Find items ready to work on az boards query --wiql "SELECT [System.Id], [System.Title], [System.State] ` FROM WorkItems ` WHERE [System.AssignedTo] = @Me ` AND [System.State] = 'Ready to Code' ` ORDER BY [Microsoft.VSTS.Common.Priority]" --output table # Activate the item az boards work-item update --id ID --state Active
2. Create a branch
git checkout -b feature/ID-short-description
3. Create a PR linked to work item
az repos pr create ` --title "feat: description" ` --description "Resolves AB#ID" ` --work-items ID ` --auto-complete true ` --delete-source-branch true
The --work-items flag links the PR. Use AB#ID in description for extra linking.
4. Monitor pipeline on PR
# List runs for the PR branch az pipelines runs list --branch feature/ID-short-description --top 1 --output table # Check run details az pipelines runs show --id RUN_ID --output table
5. Complete PR
az repos pr update --id PR_ID --status completed
Work item state transitions automatically if board rules are configured.
Assign to self
$me = az account show --query "user.name" -o tsv az boards work-item update --id ID --assigned-to $me
Planning Flow - Features, Stories, Hierarchy and Estimation
Assumes $config loaded from .\ado\config.json.
Path conventions
| Work Item Type | Area Path (config key) | Iteration Path (config key) |
|---|---|---|
| Epic | areaPaths.epic | iterationPath |
| Feature | areaPaths.feature | iterationPath |
| User Story | areaPaths.story | iterationPath |
| Task | areaPaths.task | iterationPath |
| Bug | areaPaths.bug | iterationPath |
Descriptions and acceptance criteria
| Work Item Type | Description Field | Acceptance Criteria |
|---|---|---|
| Feature | --description (put AC here) | N/A - use description |
| User Story | --description (user story text) | --fields "Microsoft.VSTS.Common.AcceptanceCriteria=..." |
Both fields accept HTML. Use <ul><li> for bullet lists:
# Feature - AC goes in description az boards work-item update --id FEATURE_ID ` --description "<h3>Acceptance Criteria</h3><ul><li>Criterion 1</li><li>Criterion 2</li></ul>" # Story - description for story, AC in its own field az boards work-item update --id STORY_ID ` --description "As a user, I want to..." ` --fields "Microsoft.VSTS.Common.AcceptanceCriteria=<ul><li>Criterion 1</li><li>Criterion 2</li></ul>"
Create a Feature under an Epic
$featureId = az boards work-item create ` --type Feature ` --title "Feature title" ` --description "Feature description" ` --area $config.areaPaths.feature ` --iteration $config.iterationPath ` --query "id" -o tsv az boards work-item relation add --id $featureId --relation-type parent --target-id EPIC_ID
Create Stories under a Feature
$storyId = az boards work-item create ` --type "User Story" ` --title "Story title" ` --description "As a user, I want..." ` --area $config.areaPaths.story ` --iteration $config.iterationPath ` --query "id" -o tsv az boards work-item relation add --id $storyId --relation-type parent --target-id FEATURE_ID
Bulk story creation
$featureId = FEATURE_ID
@("Story A", "Story B", "Story C") | ForEach-Object {
$storyId = az boards work-item create --type "User Story" --title $_ `
--area $config.areaPaths.story `
--iteration $config.iterationPath `
--query "id" -o tsv
az boards work-item relation add --id $storyId --relation-type parent --target-id $featureId
}
Set iteration and area paths
az boards work-item update --id ID ` --area $config.areaPaths.story ` --iteration $config.iterationPath
Estimation (Story Points)
Story points use the scale from config.json (default: first 10 Fibonacci numbers).
az boards work-item update --id ID ` --fields "Microsoft.VSTS.Scheduling.StoryPoints=5"
Bulk estimate stories
$estimates = @{
12345 = 3
12346 = 8
12347 = 5
}
$estimates.GetEnumerator() | ForEach-Object {
az boards work-item update --id $_.Key `
--fields "Microsoft.VSTS.Scheduling.StoryPoints=$($_.Value)" --output table
}
Complexity guide
| Points | Complexity |
|---|---|
| 1 | Trivial - config change, typo fix |
| 2 | Small - single file, well-understood change |
| 3 | Small-medium - a few files, straightforward |
| 5 | Medium - multiple files, some design needed |
| 8 | Medium-large - cross-component, some unknowns |
| 13 | Large - significant effort, multiple components |
| 21 | Very large - consider splitting |
| 34 | Epic-sized - must be split |
| 55 | Program-level - break into features |
Backlog Management - Kanban Metrics and Queries
For WIQL syntax, see WIQL Reference. For board columns and WIP limits, see Board Columns API.
WIP - Items in progress
az boards query --wiql "SELECT [System.Id], [System.Title], [System.AssignedTo] ` FROM WorkItems ` WHERE [System.State] = 'Active' ` AND [System.WorkItemType] = 'User Story' ` ORDER BY [System.ChangedDate]" --output table
Aging - Items stale in a state
Find items stuck in Active for more than 7 days:
az boards query --wiql "SELECT [System.Id], [System.Title], [System.ChangedDate] ` FROM WorkItems ` WHERE [System.State] = 'Active' ` AND [System.ChangedDate] < @Today - 7 ` AND [System.WorkItemType] = 'User Story'" --output table
Throughput - Recently completed
Items closed in the last 14 days:
az boards query --wiql "SELECT [System.Id], [System.Title], [System.ChangedDate] ` FROM WorkItems ` WHERE [System.State] = 'Closed' ` AND [System.ChangedDate] >= @Today - 14 ` AND [System.WorkItemType] = 'User Story' ` ORDER BY [System.ChangedDate] DESC" --output table
State distribution
Count items per state:
$items = az boards query --wiql "SELECT [System.Id], [System.State] `
FROM WorkItems `
WHERE [System.WorkItemType] = 'User Story' `
AND [System.State] <> 'Removed'" --output json | ConvertFrom-Json
$items | Group-Object { $_.fields.'System.State' } |
Select-Object @{N='State';E={$_.Name}}, Count |
Sort-Object Count -Descending | Format-Table
Cycle time
Query item revision history (REST):
az devops invoke ` --area wit ` --resource revisions ` --route-parameters project=$config.project workItemId=ID ` --api-version 7.0
Pipeline Debugging - Runs, Logs and Triggers
Use for pipeline status, failed builds, logs, triggering pipelines, or retrying runs.
List recent runs
az pipelines runs list --top 10 --output table az pipelines runs list --pipeline-ids PIPELINE_ID --result failed --top 5 --output table
View run details and logs
# Show run summary az pipelines runs show --id RUN_ID --output table # Get logs via REST (no direct CLI command for logs) az devops invoke ` --area pipelines ` --resource logs ` --route-parameters project=$config.project pipelineId=PIPELINE_ID runId=RUN_ID ` --api-version 7.0
Trigger and retry
# Trigger a pipeline az pipelines run --name "pipeline-name" --branch main # Trigger with variables az pipelines run --name "pipeline-name" --variables "env=staging" "deploy=true"
Download artifacts
az pipelines runs artifact download ` --run-id RUN_ID ` --artifact-name "drop" ` --path ./artifacts
View pipeline definition
az pipelines show --name "CI Build" --output yaml
Command Cookbook
Use for CLI patterns, bulk ops, REST API calls, and output formatting.
Work Items
Create
# User Story az boards work-item create ` --type "User Story" ` --title "Implement login page" ` --description "As a user, I want to log in so I can access my account." ` --assigned-to "user@example.com" ` --area $config.areaPaths.story ` --iteration $config.iterationPath # Bug az boards work-item create ` --type Bug ` --title "Login button unresponsive on mobile" ` --description "<p>Steps to reproduce:</p><ol><li>Open mobile browser</li><li>Tap login</li></ol>" ` --assigned-to "user@example.com" # Task under a Story (create then link) $taskId = az boards work-item create ` --type Task ` --title "Write unit tests for auth module" ` --query "id" -o tsv az boards work-item relation add --id $taskId --relation-type parent --target-id STORY_ID
For hierarchy creation, see Planning Flow.
Parent-Child Linking
There is no --parent flag on az boards work-item create. Use relation add after creation:
# Link child to parent az boards work-item relation add --id CHILD_ID --relation-type parent --target-id PARENT_ID # Link multiple children to same parent az boards work-item relation add --id PARENT_ID --relation-type child --target-id CHILD1,CHILD2,CHILD3 # View relations on a work item az boards work-item relation show --id 12345 --output table
Read
# Show single item az boards work-item show --id 12345 --output table # Show with all relations/links az boards work-item show --id 12345 --expand relations # Note: --fields and --expand cannot be used together
Update
# Change state az boards work-item update --id 12345 --state Active az boards work-item update --id 12345 --state Closed # Reassign az boards work-item update --id 12345 --assigned-to "other@example.com" # Update title and description az boards work-item update --id 12345 ` --title "Updated title" ` --description "Updated description" # Add tags az boards work-item update --id 12345 --fields "System.Tags=api,backend,priority" # Move to different area/iteration az boards work-item update --id 12345 ` --area $config.areaPaths.story ` --iteration $config.iterationPath # Update custom fields using --fields az boards work-item update --id 12345 ` --fields "Microsoft.VSTS.Scheduling.StoryPoints=5"
Delete
# Delete (moves to recycle bin) az boards work-item delete --id 12345 --yes # Permanently destroy (cannot undo) az boards work-item delete --id 12345 --destroy --yes
Bulk operations (PowerShell)
# Bulk close items by query
$ids = az boards query --wiql "SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Resolved'" --output json | ConvertFrom-Json
$ids | ForEach-Object { az boards work-item update --id $_.id --state Closed }
# Bulk assign
@(100, 101, 102) | ForEach-Object {
az boards work-item update --id $_ --assigned-to "dev@example.com"
}
Pull Requests
Create
# Basic PR az repos pr create ` --title "feat: add user authentication" ` --description "Implements OAuth2 login flow. Resolves AB#12345" # PR with full options az repos pr create ` --title "feat: add user authentication" ` --description "Implements OAuth2 login flow" ` --source-branch feature/12345-auth ` --target-branch main ` --work-items 12345 12346 ` --reviewers "reviewer@example.com" ` --auto-complete true ` --delete-source-branch true ` --squash true # Draft PR az repos pr create ` --title "WIP: refactoring auth module" ` --draft
Review and manage
# List active PRs az repos pr list --status active --output table # List my PRs az repos pr list --creator "user@example.com" --output table # List PRs assigned to me for review az repos pr list --reviewer "user@example.com" --output table # Show PR details az repos pr show --id 42 --output table # View PR policies/checks status az repos pr policy list --id 42 --output table # Approve (vote=10) az repos pr set-vote --id 42 --vote 10 # Approve with suggestions (vote=5) az repos pr set-vote --id 42 --vote 5 # Waiting for author (vote=-5) az repos pr set-vote --id 42 --vote -5 # Reject (vote=-10) az repos pr set-vote --id 42 --vote -10 # Reset vote (vote=0) az repos pr set-vote --id 42 --vote 0 # Complete/merge PR az repos pr update --id 42 --status completed # Abandon PR az repos pr update --id 42 --status abandoned # Add reviewer az repos pr reviewer add --id 42 --reviewers "reviewer@example.com" # Add comment (use REST via az devops invoke) az devops invoke ` --area git ` --resource pullRequestThreads ` --route-parameters project=$config.project repositoryId=REPO pullRequestId=42 ` --api-version 7.0 ` --http-method POST ` --in-file comment.json
PR work item linking
# Link work items when creating PR az repos pr create --title "fix: resolve bug" --work-items 12345 12346 # Add work item link to existing PR az repos pr work-item add --id 42 --work-items 12345 # List linked work items az repos pr work-item list --id 42 --output table
Repos
Branch management
# List branches az repos ref list --repository REPO --filter heads/ --output table # Create branch from main az repos ref create ` --name "refs/heads/feature/new-branch" ` --repository REPO ` --object-id $(az repos ref list --repository REPO --filter heads/main --query "[0].objectId" -o tsv) # Delete branch az repos ref delete ` --name "refs/heads/feature/old-branch" ` --repository REPO ` --object-id COMMIT_SHA # Lock/unlock branch az repos ref lock --name "refs/heads/release/1.0" --repository REPO az repos ref unlock --name "refs/heads/release/1.0" --repository REPO
Branch policies
# List policies on a branch az repos policy list --branch main --repository-id REPO_ID --output table # Require minimum reviewers az repos policy approver-count create ` --branch main ` --repository-id REPO_ID ` --minimum-approver-count 2 ` --creator-vote-counts false ` --enabled true ` --blocking true # Require build validation az repos policy build create ` --branch main ` --repository-id REPO_ID ` --build-definition-id PIPELINE_ID ` --display-name "CI Must Pass" ` --enabled true ` --blocking true ` --queue-on-source-update-only true
REST API via az devops invoke
# Generic GET az devops invoke ` --area wit ` --resource workitems ` --route-parameters project=$config.project id=12345 ` --api-version 7.0 # Generic POST with body from file az devops invoke ` --area wit ` --resource workitems ` --route-parameters project=$config.project ` --api-version 7.0 ` --http-method POST ` --in-file body.json # Get work item revisions (for cycle time) az devops invoke ` --area wit ` --resource revisions ` --route-parameters project=$config.project workItemId=12345 ` --api-version 7.0
Output formatting
# Table (human readable)
az boards query --wiql "..." --output table
# JSON (for processing)
az boards query --wiql "..." --output json
# TSV (for scripting)
az boards work-item show --id 12345 --query "fields.\"System.Title\"" -o tsv
# JMESPath filtering
az boards query --wiql "..." --output json `
--query "[].fields.{Id: 'System.Id', Title: 'System.Title', State: 'System.State'}"
# Pipe JSON to PowerShell
$items = az boards query --wiql "..." --output json | ConvertFrom-Json
$items | Where-Object { $_.fields.'System.State' -eq 'Active' } | Format-Table
Board Columns API (REST)
Use for board columns, WIP limits, or Kanban column configuration. For board-level metrics, see Backlog Management.
The az boards CLI does not expose board column configuration directly. Use az devops invoke.
Step 1: List boards for a team
Get board IDs for the team:
az devops invoke `
--area work `
--resource boards `
--route-parameters project=$config.project team="{TEAM_NAME}" `
--api-version 7.0 --output json
Returns board metadata with IDs:
{
"count": 5,
"value": [
{ "id": "...", "name": "Stories" },
{ "id": "...", "name": "Epics" },
{ "id": "...", "name": "Features" }
]
}
Step 2: Fetch columns for a specific board
Pass the board id to get column details including WIP limits:
$boardId = "{BOARD_ID}"
$result = az devops invoke `
--area work `
--resource boards `
--route-parameters project=$config.project `
team="{TEAM_NAME}" `
id=$boardId `
--api-version 7.0 --output json
$board = $result | ConvertFrom-Json
foreach ($col in $board.columns) {
$wip = if ($col.itemLimit -and $col.itemLimit -gt 0) { $col.itemLimit } else { "none" }
Write-Host ("{0,-30} WIP Limit: {1}" -f $col.name, $wip)
}
Gotchas
- •Do NOT use
az restfor this endpoint; responses can include Unicode (infinity symbol U+221E) that triggers Windowscharmaperrors. - •Use
az devops invokeinstead, which handles encoding correctly. - •The list endpoint (
--resource boardswithoutid) returns metadata only; pass the boardidto get column details. - •Set
$env:PYTHONIOENCODING = "utf-8"as a safety net for encoding issues.
Step 3: Query work items by board column
Board columns like "PR", "Testing", "Deployment" may share the same underlying state (e.g., "Active"). Use System.BoardColumn for precise column queries:
az boards query --wiql "SELECT [System.Id], [System.Title], [System.AssignedTo], [System.BoardColumn] ` FROM WorkItems ` WHERE [System.WorkItemType] = 'User Story' ` AND [System.AreaPath] UNDER $config.areaPaths.story ` AND [System.BoardColumn] = 'PR' ` AND [System.State] <> 'Removed' ` ORDER BY [System.ChangedDate] DESC" --output table
Available board column values
These match the column names from Step 2. For the Stories board:
| Board Column | Underlying State | WIP Limit |
|---|---|---|
| New | New | none |
| Ready | Ready to Code | none |
| Active | Active | 8 |
| PR | Active | 5 |
| Testing | Active | 3 |
| Deployment | Active | 3 |
| Closed | Closed | none |
Key insight
Multiple board columns can map to the same ADO state (e.g., "PR", "Testing", "Deployment" all map to "Active"). Querying by System.State = 'Active' returns items across all those columns. Use System.BoardColumn for column-level precision.
Check WIP compliance
Compare column item count against WIP limit:
$columns = @("Active", "PR", "Testing", "Deployment")
foreach ($col in $columns) {
$items = az boards query --wiql "SELECT [System.Id] FROM WorkItems `
WHERE [System.WorkItemType] = 'User Story' `
AND [System.AreaPath] UNDER $config.areaPaths.story `
AND [System.BoardColumn] = '$col' `
AND [System.State] <> 'Removed'" --output json | ConvertFrom-Json
Write-Host "$col : $($items.Count) items"
}
API reference
- •Boards - Get -
GET {org}/{project}/{team}/_apis/work/boards/{board}?api-version=7.0 - •Boards - List -
GET {org}/{project}/{team}/_apis/work/boards?api-version=7.0 - •WIQL BoardColumn -
System.BoardColumnfield reference
WIQL Reference
Use for field names, operators, macros, quoting rules, or link query syntax. WIQL is used with az boards query --wiql "...".
Clause structure
SELECT [Field1], [Field2] FROM WorkItems -- or WorkItemLinks for link queries WHERE [Condition1] AND [Condition2] ORDER BY [Field] ASC|DESC ASOF 'YYYY-MM-DD' -- optional: historical point-in-time query
- •
SELECT *is not supported; list each field explicitly. - •Use
FROM WorkItemsfor flat queries,FROM WorkItemLinksfor link/hierarchy queries. - •
ASOFmust be the last clause and is not compatible with link queries.
Field names
Always use bracket syntax: [System.FieldName].
Common fields
| Field | Description |
|---|---|
[System.Id] | Work item ID |
[System.Title] | Title (single-line text) |
[System.State] | Current state (New, Active, Resolved, Closed, Removed) |
[System.WorkItemType] | Type (Epic, Feature, User Story, Task, Bug) |
[System.AssignedTo] | Assigned person (identity field) |
[System.CreatedDate] | Creation date |
[System.ChangedDate] | Last modified date |
[System.CreatedBy] | Creator (identity field) |
[System.Tags] | Tags (comma-separated string) |
[System.AreaPath] | Area path (tree path) |
[System.IterationPath] | Iteration path (tree path) |
[System.Description] | Description (HTML/rich-text) |
[System.TeamProject] | Project name |
[System.BoardColumn] | Current Kanban column |
[System.BoardColumnDone] | Whether in done split of column |
Priority and effort
| Field | Description |
|---|---|
[Microsoft.VSTS.Common.Priority] | Priority (1=Critical, 2=High, 3=Medium, 4=Low) |
[Microsoft.VSTS.Common.Severity] | Bug severity |
[Microsoft.VSTS.Scheduling.StoryPoints] | Story points |
[Microsoft.VSTS.Scheduling.Effort] | Effort |
[Microsoft.VSTS.Common.ValueArea] | Business or Architectural |
Dates and history
| Field | Description |
|---|---|
[Microsoft.VSTS.Common.ClosedDate] | Date item was closed |
[Microsoft.VSTS.Common.ResolvedDate] | Date item was resolved |
[Microsoft.VSTS.Common.ActivatedDate] | Date item was activated |
[Microsoft.VSTS.Common.StateChangeDate] | Last state change date |
Operators
Comparison
| Operator | Field Types | Example |
|---|---|---|
= | All | [System.State] = 'Active' |
<> | All | [System.State] <> 'Removed' |
>, <, >=, <= | Number, Date | [Microsoft.VSTS.Common.Priority] <= 2 |
IN | String, Number | [System.State] IN ('New', 'Active') |
NOT IN | String, Number | [System.State] NOT IN ('Closed', 'Removed') |
Text search
| Operator | Field Types | Indexed | Example |
|---|---|---|---|
CONTAINS | String (single-line) | No | [System.Title] CONTAINS 'auth' |
NOT CONTAINS | String (single-line) | No | [System.Title] NOT CONTAINS 'test' |
CONTAINS WORDS | String, PlainText, HTML | Yes | [System.Description] CONTAINS WORDS 'authentication failed' |
NOT CONTAINS WORDS | String, PlainText, HTML | Yes | [System.Description] NOT CONTAINS WORDS 'obsolete' |
Notes:
- •
CONTAINSis a slow unindexed substring scan. - •
CONTAINS WORDSis indexed, faster, and supports stemming. - •Use
CONTAINS WORDSfor Description/repro steps; useCONTAINSfor Title/Tags. - •
CONTAINS WORDSlimit: 100 characters max in search phrase.
Path operators
| Operator | Field Types | Example |
|---|---|---|
UNDER | AreaPath, IterationPath | [System.AreaPath] UNDER '{AREA_PATH}' |
NOT UNDER | AreaPath, IterationPath | [System.IterationPath] NOT UNDER '{ITERATION_PATH}' |
UNDER matches the path itself and all children beneath it.
Identity operators
| Operator | Field Types | Example |
|---|---|---|
IN GROUP | Identity fields | [System.AssignedTo] IN GROUP 'My Team' |
NOT IN GROUP | Identity fields | [System.AssignedTo] NOT IN GROUP 'Contractors' |
IN GROUP checks membership in an ADO security group or team.
Historical operators
| Operator | Field Types | Example |
|---|---|---|
WAS EVER | Identity, State, picklist | [System.AssignedTo] WAS EVER 'alice@example.com' |
EVER | State | [System.State] EVER 'Active' |
WAS EVER checks if a field ever had a value at any point in history.
Null checks
| Operator | Example |
|---|---|
= '' | [System.AssignedTo] = '' (unassigned) |
<> '' | [System.AssignedTo] <> '' (assigned) |
Macros
| Macro | Description |
|---|---|
@Me | Current authenticated user |
@Today | Today's date (midnight, server timezone) |
@Today - N | N days ago |
@Today + N | N days from now |
@Project | Current project name |
@CurrentIteration | Current team iteration (requires team context) |
@CurrentIteration + N | N iterations ahead |
@CurrentIteration - N | N iterations back |
Warning: @CurrentIteration requires team context and may not resolve in all CLI contexts. Use explicit iteration paths when querying via az boards query.
Query patterns
Basic SELECT
SELECT [System.Id], [System.Title], [System.State] FROM WorkItems WHERE [System.WorkItemType] = 'User Story' AND [System.State] = 'Active' ORDER BY [Microsoft.VSTS.Common.Priority]
Historical query (ASOF)
SELECT [System.Id], [System.Title], [System.State], [System.AssignedTo] FROM WorkItems WHERE [System.WorkItemType] = 'User Story' AND [System.State] = 'Active' ORDER BY [System.ChangedDate] DESC ASOF '2025-01-15'
- •Date format:
'YYYY-MM-DD'(ISO 8601 recommended). - •Cannot be used with
FROM WorkItemLinks(flat queries only). - •Returns work item data as it existed on that date.
Hierarchical (Parent-Child)
WARNING: az boards query silently returns empty results for FROM WorkItemLinks. Use REST API via az devops invoke instead:
# Save WIQL to a temp file (required for POST body)
$body = '{"query": "SELECT [System.Id], [System.Title] FROM WorkItemLinks WHERE (Source.[System.Id] = 12345) AND ([System.Links.LinkType] = ''System.LinkTypes.Hierarchy-Forward'') MODE (MustContain)"}'
$body | Out-File -Encoding utf8 wiql-query.json
az devops invoke `
--area wit `
--resource wiql `
--http-method POST `
--api-version 7.0 `
--route-parameters project=$config.project `
--in-file wiql-query.json `
--output json
Remove-Item wiql-query.json
The response contains workItemRelations with source/target ID pairs; fetch full work item details separately.
For flat queries that find children without link syntax, use:
SELECT [System.Id], [System.Title], [System.WorkItemType]
FROM WorkItems
WHERE [System.WorkItemType] = 'Feature'
AND [System.AreaPath] UNDER '{AREA_PATH}'
Or use az boards work-item relation show --id PARENT_ID to list children directly.
Link query WIQL syntax (REST API)
Find all stories under all features:
SELECT [System.Id], [System.Title], [System.WorkItemType] FROM WorkItemLinks WHERE (Source.[System.WorkItemType] = 'Feature') AND ([System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward') AND (Target.[System.WorkItemType] = 'User Story') MODE (Recursive)
Find all descendants of a specific work item:
SELECT [System.Id], [System.Title], [System.WorkItemType] FROM WorkItemLinks WHERE (Source.[System.Id] = 12345) AND ([System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward') MODE (Recursive, MustContain)
Related links
SELECT [System.Id], [System.Title] FROM WorkItemLinks WHERE (Source.[System.Id] = 12345) AND ([System.Links.LinkType] = 'System.LinkTypes.Related-Forward') MODE (MustContain)
Link query modes
| Mode | Behavior |
|---|---|
MustContain | Only return items that have matching links |
MayContain | Return items regardless of whether they have matching links |
DoesNotContain | Only return items that do NOT have matching links |
Recursive | Traverse hierarchy recursively (all descendants) |
Modes can be combined: MODE (Recursive, MustContain)
Common link types
| Link Type | Description |
|---|---|
System.LinkTypes.Hierarchy-Forward | Parent -> Child |
System.LinkTypes.Hierarchy-Reverse | Child -> Parent |
System.LinkTypes.Related-Forward | Related item |
System.LinkTypes.Dependency-Forward | Successor (depends on) |
System.LinkTypes.Dependency-Reverse | Predecessor |
List all link types in your org: az boards work-item relation list-type --output table
az boards query usage
# Flat query az boards query --wiql "SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.State] = 'Active'" --output table # Output as JSON for processing az boards query --wiql "SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Active'" --output json # Query by saved query ID (from ADO web portal) az boards query --id QUERY_ID --output table # Query with org/project override az boards query --wiql "..." --org https://dev.azure.com/ORG --project PROJECT
PowerShell quoting for WIQL
Rules:
- •Outer quotes: use double quotes
"to wrap the--wiqlvalue. - •Inner string values: use single quotes inside WIQL.
- •Escape single quotes in values by doubling them.
Examples:
# Standard query - double outside, single inside
az boards query --wiql "SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Active'" --output table
# Value containing a single quote (O'Brien)
az boards query --wiql "SELECT [System.Id] FROM WorkItems WHERE [System.Title] CONTAINS 'O''Brien'"
# Multi-line query in PowerShell (use backtick for continuation)
az boards query --wiql "SELECT [System.Id], [System.Title], [System.State] `
FROM WorkItems `
WHERE [System.WorkItemType] = 'User Story' `
AND [System.State] = 'Active' `
AND [System.AreaPath] UNDER '{AREA_PATH}' `
ORDER BY [Microsoft.VSTS.Common.Priority]" --output table
# Store WIQL in a variable for complex queries
$wiql = @"
SELECT [System.Id], [System.Title], [System.State]
FROM WorkItems
WHERE [System.WorkItemType] = 'User Story'
AND [System.State] IN ('New', 'Active')
AND [System.AreaPath] UNDER '{AREA_PATH}'
ORDER BY [System.ChangedDate] DESC
"@
az boards query --wiql $wiql --output table
Tip: For complex queries, the here-string (@"..."@) approach avoids quoting issues.
Gotchas
Quoting and syntax
- •String values use single quotes inside WIQL:
'Active'not"Active". - •To embed a literal single quote, double it:
'O''Brien'. - •Field names are case-insensitive but bracket syntax
[...]is required. - •
SELECT *is not supported - always list fields explicitly.
Operators
- •
CONTAINSis a slow unindexed substring scan - avoid on large result sets. - •
CONTAINS WORDSis fast indexed full-text - prefer for Description/repro steps. - •
CONTAINS WORDSsupports stemming ("run" matches "running"). - •
UNDERmatches the path itself and all children beneath it. - •
WAS EVERworks on identity and picklist fields, not just[System.State].
Dates
- •Date comparisons use server timezone, not local timezone.
- •No date arithmetic between fields; compute date diffs outside WIQL.
- •
@Todayresolves to midnight server time.
Limits and restrictions
- •Maximum 20,000 results per query.
- •
ORDER BYsupports one field, ascending (default) orDESC. - •
ASOFis not compatible withFROM WorkItemLinks(flat queries only). - •
az boards querysilently returns empty forFROM WorkItemLinks- use REST API viaaz devops invoke. - •
@CurrentIterationrequires team context and may fail in CLI without team scope. - •REST API WIQL endpoint returns only IDs; a second call is needed to fetch field values.
- •Area/iteration paths should not contain quotes (no escape mechanism).
- •
--fieldsand--expandcannot be used together onaz boards work-item show.