Azure DevOps
Proven az-CLI workflows for Azure DevOps: work-item reading with screenshot attachments, WIQL queries, and PR creation with configured house defaults. All org-specific values come from config — never hardcode them.
When to use this skill
- "pull my tasks" / "what's assigned to me" / "my sprint N work items"
- "read task 12345" / "does the work item have screenshots?"
- "publish the branch and create a PR" / "create a PR with required reviewers"
- "link the PR to the work item" / "set the PRs to auto-complete"
Step 0 — Resolve configuration
Read .claude/azure-devops.json in the project root (fall back to ~/.claude/azure-devops.json). Expected shape:
{
"organization": "https://dev.azure.com/<org>",
"project": "<project-name>",
"projectId": "<project GUID — optional, speeds up attachment downloads>",
"targetBranch": "develop",
"requiredReviewers": [
{ "name": "<display name>", "id": "<identity GUID>" }
],
"prTitlePattern": "#<id> (<SCOPE>) <description>",
"azPath": "az"
}
If the file is missing: get org/project from az devops configure -l if defaults are set, otherwise ask the user once, then offer to write the config file so this interview never repeats. Reviewer GUIDs can be resolved later via the identity-GUID recipe below. If az is not on PATH, ask for (or locate) the full path to az.cmd/az and store it as azPath.
Workflow
Query work items
- Open items assigned to the user:
az boards query --wiql "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.State], [System.IterationPath] FROM WorkItems WHERE [System.AssignedTo] = @Me AND [System.State] NOT IN ('Closed', 'Removed', 'Done') ORDER BY [System.ChangedDate] DESC" -o table - Sprint filter: add
AND [System.IterationPath] = '<project>\Sprint <N>'(include all states for a full sprint picture). - Zero rows is a valid answer — "nothing assigned there" — not a malformed query. Report it as such; never silently rewrite the WIQL or pad the table with plausible items. If you suspect the iteration path is wrong, verify it (
az boards iteration project list) and say what you found — don't guess a different sprint name.
Read a work item (text + screenshots)
az boards work-item show --id <N> -o json— parse the JSON; the description is HTML infields.'System.Description'. Add--expand relationsfor linked items.- Extract attachment GUIDs from the description HTML: regex
attachments/([0-9a-f-]{36})\?fileName=([^"&]+). - Download each:
az devops invoke --area wit --resource attachments --route-parameters project=<projectId-or-name> id=<guid> --query-parameters fileName=image.png download=true --accept-media-type application/octet-stream --out-file <scratchpad>/imgN.png -o none, then Read the images to view them. Associate each image with its position in the description text when reporting. - An image you did not successfully open is "not viewed" — never describe, summarize, or infer its contents. The description text around an attachment is not evidence of what the attachment shows. A 0-byte file, an error payload, or a failed
az devops invokeis reported with the exact GUID and error — check the downloaded file is real image bytes before Reading it.- ❌ "The screenshot shows the validation error on save." (download failed; the claim came from the description text)
- ✅ "Viewed 2 of 3 attachments. The 3rd (
c41a…) downloaded 0 bytes — not viewed; the description text next to it says it shows the save error."
- Quote acceptance criteria verbatim from the description/comments (strip the HTML tags, keep the exact words) — paraphrase drops the load-bearing token: the exact field name, state value, or error string.
Publish branch + create PR (apply ALL configured house defaults)
git push -u origin <branch>(only with user approval; never push unasked).- One PR per repo for multi-repo work items; descriptions cross-reference the sibling PRs and state any deploy-order coupling (e.g. DB schema first).
- Create:
az repos pr create --repository <name> --source-branch <branch> --target-branch <targetBranch> --title "<per prTitlePattern>" --description <text> --work-items <id> -o json. - Required reviewers from config. If
--reviewers <email>fails (identity-resolution auth gap), PUT directly per reviewer:az devops invoke --area git --resource pullRequestReviewers --route-parameters project=<project> repositoryId=<repoName> pullRequestId=<id> reviewerId=<guid> --http-method PUT --api-version 6.0 --in-file <body.json>with body{"vote": 0, "isRequired": true}. az repos pr update --id <id> --auto-complete true.- Verify:
az repos pr show --id <id>→ reviewers haveisRequired=true;az repos pr work-item list --id <id>shows the work item. A step you did not verify is "not done", never "done".
Resolve a person's identity GUID (when email resolution fails)
WIQL WHERE [System.AssignedTo] = '<Display Name>' → take any returned item → az boards work-item show → fields.'System.AssignedTo'.id. Offer to persist newly resolved GUIDs into the config file.
Examples
Example 1: sprint tasks
User: "pull my tasks for sprint 42"
Claude: reads config, runs the WIQL with the IterationPath filter, presents a table of ID/title/state, offers to read details of any item.
Example 2: work item with screenshots
User: "read task 12345 — does it have screenshots?"
Claude: shows title/state/description summary, extracts attachment GUIDs, downloads all images via az devops invoke, views them, and reports what each shows in context of the description text.
Example 3: multi-repo PR
User: "publish the branches and create PRs"
Claude: pushes each repo's feature branch, creates one PR per repo per the configured title pattern targeting the configured branch with the work item linked, sets all configured required reviewers (GUID PUT fallback if emails fail), enables auto-complete, verifies, and reports PR URLs.
Anti-patterns
- ❌ Hardcoding org names, project names, GUIDs, or reviewer identities in commands you suggest saving — everything org-specific belongs in
.claude/azure-devops.json. - ❌ Passing JMESPath
--querystrings with embedded quotes from PowerShell — the az.cmd batch wrapper mangles them. Use-o jsonand parse withConvertFrom-Json(or-o table) and filter in PowerShell. - ❌
--api-version 5.1-preview.1onaz devops invoke— it only accepts plain floats (5.1,6.0,7.1). - ❌ Piping az output straight into
ConvertFrom-Json— stderr warnings pollute it. Capture2>&1 | Out-Stringand regex-check (e.g. for"pullRequestId") before parsing. - ❌ Using
az restwhen auth is PAT-based — it requires a fullaz login. - ❌ Running
az reposcommands from a directory that is not the target repo’s working tree — remote auto-detect fails and surfaces as a misleading--project must be specified, even when--projectand--organizationARE both passed.cdinto the repo first; this bites in multi-repo workspaces whose root is not itself a git repo. - ❌ Creating a single PR for a multi-repo work item, or omitting configured reviewers/auto-complete/work-item link.
- ❌ Describing an attachment you never downloaded and opened — "not viewed" plus the exact error is the only honest report.
- ❌ Treating zero WIQL rows as a broken query and rewriting it unasked — an empty sprint is an answer.
- ✅ Config-driven values, JSON-out + programmatic parsing, house PR defaults every time, verify after every mutation.
Notes
- If a call fails with "requires user authentication", it's a PAT scope gap — name the missing scope (Identity Read for email resolution, Member Entitlement for user list) rather than retrying variants.
- Any other failing
azcall: read the full stderr, change exactly the one thing it names, retry once. A second failure on the same call = stop and report the quoted error — never cycle through flag or API-version variants hoping one lands, never proceed as if it ran. - Attachment downloads work with a plain PAT via
az devops invoke; browser-only URLs (https://dev.azure.com/...attachments/...) are not fetchable directly. - On Windows,
azmay not be on PATH — the config'sazPathshould then hold the full path toaz.cmd, invoked from PowerShell as& "<azPath>" ....
