This Skill uses Analytics to aggregate data server-side and generate the team's metrics for the most recent N (default 6)completed Sprints. Read-only; it does not modify any data.
Project Selection
-
If the user provides a project name (for example "Contoso project"), use it directly and do not call the project list API.
-
If not provided, ask the user to provide it once.
-
If still not provided, list the projects for the user to choose from:
curl -sS -u ":$AZURE_DEVOPS_PAT" \ "https://dev.azure.com/$AZURE_DEVOPS_ORG/_apis/projects?%24top=500&api-version=7.1" -
Do not continue until the user provides or selects a project.
Team Selection
-
If the user provides a team name, use it directly and do not call the team list API.
-
If not provided, ask the user to provide it once.
-
If still not provided, list the teams in the selected project for the user to choose from:
curl -sS -u ":$AZURE_DEVOPS_PAT" \ "https://dev.azure.com/$AZURE_DEVOPS_ORG/_apis/projects/{project}/teams?%24top=500&api-version=7.1" -
Do not continue until the user provides or selects a team.
Connection and Authentication
Always use curl to call the Azure DevOps REST API directly; each command carries its own authentication parameters.
- Organization: Read the environment variable
AZURE_DEVOPS_ORG(organization name, for examplecontoso). If it is not set, ask the user once; if the user gives a URL, extract the organization name fromhttps://dev.azure.com/{org}orhttps://{org}.visualstudio.com. - Host: Besides
dev.azure.com, the Analytics OData host isanalytics.dev.azure.com。 - Token: Send the PAT using Basic authentication:
-u ":$AZURE_DEVOPS_PAT"; if onlyAZURE_DEVOPS_EXT_PATis set (the variable used by the Azure CLI azure-devops extension), use that instead. When switching to a Microsoft Entra ID token, replace it with-H "Authorization: Bearer $AZURE_DEVOPS_TOKEN". If neither is set, ask the user to setAZURE_DEVOPS_PATin their own terminal and retry, then stop. Read the token only from the environment variable: do not ask the user to paste the token into the conversation, and do not print it or write it to a file. - PAT Permission Scopes: Analytics (Read); reading process configuration and team settings additionally requires Work Items (Read) and Project and Team (Read).
- Encoding: URL-encode project and team names in paths (write spaces as
%20); in query strings, write$as%24(for example%24top) to avoid shell expansion. For OData parameters, use-G --data-urlencodeto let curl handle encoding (see below). - Errors: HTTP 401, 403, 203, or an HTML login page in the response means the token is invalid, expired, or lacks permissions; report it and stop. HTTP 404 means the organization or project name is wrong. When Analytics returns 401 or 403, it may also be that the PAT lacks Analytics (Read) or the user does not have the "View analytics" permission. HTTP 410 means this OData version is no longer available. The
error.messageof an HTTP 400 indicates the problem, for exampleVS403522means the property does not exist in this project (common for estimation fields, see step 2). - Windows PowerShell: Call
curl.exe(rather than thecurlalias); write the environment variable as$env:AZURE_DEVOPS_PAT; PowerShell does not support<<'EOF'; first write the JSON body to a temporary file, then use--data-binary "@body.json"Submit.
Analytics Query Rules
- Base URL:
https://analytics.dev.azure.com/$AZURE_DEVOPS_ORG/{project}/_odata/v4.0-preview/。v4.0-previewOn 404 or 410, switch in order tov3.0-preview、v2.0(the queries in this Skill have the same syntax in these versions). - With
curl -Gcombined with--data-urlencodeto pass OData parameters; parameters are written in double quotes, and$filter、$apply、$count's$is written as\$(PowerShell uses backtick escaping instead:`$filter)。 - Do not use
$filterand$applyat the same time in the same query: if you want to aggregate, put the conditions into$apply=filter(...)/groupby(...); if you want item-by-item data, use$filterplus$select. Whenever aggregation can be done server-side, use$apply, and do not pull the entire batch of work items back to the local machine for calculation. - Use single quotes for strings; single quotes within names are written as two (
'O''Brien Team'); GUIDs (for exampleIterationSK) are not quoted. - Filter by team:
Teams/any(t: t/TeamName eq 'Contoso Team')(determined by the team's area path). - When the response has
@odata.nextLink, it means there is a next page; fetch again using that URL.@vsts.warningsInVS403507is only a reminder that the query has no$selector$apply, and does not affect the results. - Dates are in the organization's time zone (for example,
2026-01-16T23:59:59.999+08:00);...DateSKisYYYYMMDDinteger.
Steps
-
Determine the project and team according to the rules above. N defaults to 6; if the user specifies it, use that.
-
Determine the work item type and estimation attribute:
curl -sS -u ":$AZURE_DEVOPS_PAT" \ "https://dev.azure.com/$AZURE_DEVOPS_ORG/{project}/_apis/work/processconfiguration?api-version=7.1" curl -sS -u ":$AZURE_DEVOPS_PAT" \ "https://dev.azure.com/$AZURE_DEVOPS_ORG/{project}/{team}/_apis/work/teamsettings?api-version=7.1"- Type:
requirementBacklog.workItemTypes[].name; when the team-configuredbugsBehaviorisasRequirements, also addbugWorkItems.workItemTypes[].name。 - Estimation attribute: map
typeFields.Effort.referenceNameto the Analytics attribute:Microsoft.VSTS.Scheduling.StoryPoints→StoryPoints、Microsoft.VSTS.Scheduling.Effort→Effort、Microsoft.VSTS.Scheduling.Size→Size; for custom fields, replace.with_(Custom.Estimate→Custom_Estimate). When the query returnsVS403522, remove the estimation attribute, report only Throughput, and explain why.
- Type:
-
Get the team's most recent N completed Iterations. Determine by Analytics's
IsEnded, do not use REST'stimeFrame(when there is no new Sprint, REST may mark the last completed Sprint ascurrent):curl -sS -u ":$AZURE_DEVOPS_PAT" -G \ "https://analytics.dev.azure.com/$AZURE_DEVOPS_ORG/{project}/_odata/v4.0-preview/Iterations" \ --data-urlencode "\$filter=Teams/any(t: t/TeamName eq 'Contoso Team') and IsEnded eq true" \ --data-urlencode "\$select=IterationSK,IterationPath,IterationName,StartDate,EndDate" \ --data-urlencode "\$orderby=EndDate desc" \ --data-urlencode "\$top=6"When there are no results, explain that this team has no completed Iterations (or the Iterations have no dates set), then stop.
-
Velocity and Throughput(one query;
IterationSK in (...)fill in all of step 3'sIterationSK):curl -sS -u ":$AZURE_DEVOPS_PAT" -G \ "https://analytics.dev.azure.com/$AZURE_DEVOPS_ORG/{project}/_odata/v4.0-preview/WorkItems" \ --data-urlencode "\$apply=filter(Teams/any(t: t/TeamName eq 'Contoso Team') and StateCategory eq 'Completed' and WorkItemType in ('Product Backlog Item','Bug') and IterationSK in ({iterationSK1},{iterationSK2}))/groupby((Iteration/IterationPath, Iteration/EndDate), aggregate(\$count as Count, Effort with sum as TotalEstimate))"Countis Throughput,TotalEstimateis Velocity. Calculate based on the work item'scurrentIteration assignment: items moved to another Sprint only after completion will count toward the new Sprint. Iterations that do not appear in the response mean 0 completed;TotalEstimatemissing or beingnullmeans none of the completed items have estimates. -
Cycle time and Lead time(item-by-item data, with you calculating the statistics; the date range goes from the earliest Iteration's
StartDateto the latest Iteration'sEndDate):curl -sS -u ":$AZURE_DEVOPS_PAT" -G \ "https://analytics.dev.azure.com/$AZURE_DEVOPS_ORG/{project}/_odata/v4.0-preview/WorkItems" \ --data-urlencode "\$filter=Teams/any(t: t/TeamName eq 'Contoso Team') and StateCategory eq 'Completed' and WorkItemType in ('Product Backlog Item','Bug') and CompletedDateSK ge 20260105 and CompletedDateSK le 20260327" \ --data-urlencode "\$select=WorkItemId,Title,WorkItemType,CycleTimeDays,LeadTimeDays,CompletedDateSK"CycleTimeDaysis the number of days from entering In Progress to completion,LeadTimeDaysis the number of days from creation to completion. Calculate the average, median, and P85 respectively (sort from smallest to largest, take the ⌈0.85 × n⌉th item); when the value isnullitems (for example, completed without going through In Progress) and note the count. When the distribution is skewed (mean much greater than the median), list the 3 longest items as outliers. -
Carry-over: the number of items still in that Iteration and not completed on the day each Iteration ends. Use
WorkItemSnapshotto get the end date (write the date part ofEndDateasDateSK) for the daily snapshot:curl -sS -u ":$AZURE_DEVOPS_PAT" -G \ "https://analytics.dev.azure.com/$AZURE_DEVOPS_ORG/{project}/_odata/v4.0-preview/WorkItemSnapshot" \ --data-urlencode "\$apply=filter(Teams/any(t: t/TeamName eq 'Contoso Team') and WorkItemType in ('Product Backlog Item','Bug') and StateCategory ne 'Completed' and StateCategory ne 'Removed' and ((IterationSK eq {iterationSK1} and DateSK eq 20260116) or (IterationSK eq {iterationSK2} and DateSK eq 20260130)))/groupby((Iteration/IterationPath, DateSK), aggregate(\$count as Count))"An Iteration that does not appear in the response means its Carry-over is 0. When the user wants to see which items were delayed in a certain Iteration, use
$filterinstead (same conditions, keeping only one set ofIterationSKandDateSK) plus\$select=WorkItemId,Title,State. Completion rate = Throughput ÷ (Throughput + Carry-over). -
Present in the format below.
Present the results
-
Title: project, team, number of Iterations covered and date range, estimation attribute (for example,
StoryPoints) and included work item types. -
Metrics for each Sprint(oldest to newest):
Sprint Period Velocity Throughput Carry-over Completion rate Sprint 12 2026/01/05–2026/01/16 21 8 2 80% The last row shows the average. When there is no estimate, the Velocity column shows (no estimate)。
-
Cycle time and Lead time: average, median, P85 (days, to one decimal place) and sample size; when there are outliers, list the ID (link
https://dev.azure.com/{organization}/{project}/_workitems/edit/{ID}) and title. -
Observations: use 2–4 points to explain trends (for example, Velocity fluctuation, high Carry-over, insufficient estimation), and state that this is an AI-generated interpretation and that the numbers in the table above prevail.
-
Dates are displayed as
YYYY/MM/DD; Analytics dates are already in the organization time zone, so take the date part directly.