Installation

Install with CLI Recommended
gh skills-hub install azure-cost

Don't have the extension? Run gh extension install samueltauil/skills-hub first.

Download and extract to your repository:

.github/skills/azure-cost/

Extract the ZIP to .github/skills/ in your repo. The folder name must match azure-cost for Copilot to auto-discover it.

Skill Files (21)

SKILL.md 7.3 KB
---
name: azure-cost
description: "Unified Azure cost management: query historical costs, forecast future spending, and optimize to reduce waste. WHEN: \"Azure costs\", \"Azure spending\", \"Azure bill\", \"cost breakdown\", \"cost by service\", \"cost by resource\", \"how much am I spending\", \"show my bill\", \"monthly cost summary\", \"cost trends\", \"top cost drivers\", \"actual cost\", \"amortized cost\", \"forecast spending\", \"projected costs\", \"estimate bill\", \"future costs\", \"budget forecast\", \"end of month costs\", \"how much will I spend\", \"optimize costs\", \"reduce spending\", \"find cost savings\", \"orphaned resources\", \"rightsize VMs\", \"cost analysis\", \"reduce waste\", \"unused resources\", \"optimize Redis costs\", \"cost by tag\", \"cost by resource group\", \"AKS cost analysis add-on\", \"namespace cost\", \"cost spike\", \"anomaly\", \"budget alert\", \"AKS cost visibility\". DO NOT USE FOR: deploying resources, provisioning infrastructure, diagnostics, security audits, or estimating costs for new resources not yet deployed."
license: MIT
metadata:
  author: Microsoft
  version: "1.0.0"
---

# Azure Cost Management Skill

Unified skill for all Azure cost management tasks: querying historical costs, forecasting future spending, and optimizing to reduce waste.

## When to Use This Skill

Activate this skill when user wants to:
- Query or analyze Azure costs (how much am I spending, show my bill, cost breakdown)
- Break down costs by service, resource, location, or tag
- Analyze cost trends over time
- Forecast future Azure spending or project end-of-month costs
- Optimize Azure costs, reduce spending, or find savings
- Find orphaned or unused resources
- Rightsize Azure VMs, containers, or services
- Generate cost optimization reports

## Quick Reference

| Property | Value |
|----------|-------|
| **Query API Endpoint** | `POST {scope}/providers/Microsoft.CostManagement/query?api-version=2023-11-01` |
| **Forecast API Endpoint** | `POST {scope}/providers/Microsoft.CostManagement/forecast?api-version=2023-11-01` |
| **MCP Tools** | `azure__documentation`, `azure__extension_cli_generate`, `azure__get_azure_bestpractices` |
| **CLI** | `az rest`, `az monitor metrics list`, `az resource list` |
| **Required Role** | Cost Management Reader + Monitoring Reader + Reader on scope |

## MCP Tools

| Tool | Description | Parameters | When to Use |
|------|-------------|------------|-------------|
| `azure__documentation` | Search Azure documentation | `query` (Required): search terms | Research Cost Management API parameters and options |
| `azure__extension_cli_generate` | Generate Azure CLI commands | `intent` (Required): task description, `cli-type` (Required): `"az"` | Construct `az rest` commands for cost queries |
| `azure__get_azure_bestpractices` | Get Azure best practices | `intent` (Required): optimization context | Inform query design with cost management best practices |
| `azure__extension_azqr` | Run Azure Quick Review compliance scan | `subscription` (Required): subscription ID, `resource-group` (Optional): resource group name | Find orphaned resources and cost optimization opportunities |
| `azure__aks` | Azure Kubernetes Service operations | varies by sub-command | AKS cost analysis: list clusters, get node pools, inspect configuration |

> ๐Ÿ’ก **Tip:** Prefer MCP tools over direct CLI commands. Use `az rest` only when MCP tools don't cover the specific operation.

---

## Routing

Read the user's request and follow the appropriate workflow below.

| User Intent | Workflow | Example Prompts |
|-------------|----------|-----------------|
| Understand current costs | [Cost Query Workflow](cost-query/workflow.md) | "how much am I spending", "cost by service", "show my bill" |
| Reduce costs / find waste | [Cost Optimization Workflow](cost-optimization/workflow.md) | "optimize costs", "find orphaned resources", "reduce spending" |
| Project future costs | [Cost Forecast Workflow](cost-forecast/workflow.md) | "forecast costs", "end of month estimate", "how much will I spend" |
| Full cost picture | All three workflows combined | "give me the full picture of my Azure costs" |

> **Important:** When optimizing costs, always present the total bill and cost breakdown alongside optimization recommendations.

---

## Scope Reference (Shared Across All Workflows)

| Scope | URL Pattern |
|-------|-------------|
| Subscription | `/subscriptions/<subscription-id>` |
| Resource Group | `/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>` |
| Management Group | `/providers/Microsoft.Management/managementGroups/<management-group-id>` |
| Billing Account | `/providers/Microsoft.Billing/billingAccounts/<billing-account-id>` |
| Billing Profile | `/providers/Microsoft.Billing/billingAccounts/<billing-account-id>/billingProfiles/<billing-profile-id>` |

> ๐Ÿ’ก **Tip:** These are scope paths only โ€” not complete URLs. Combine with the API endpoint and version.

---

## Part 1: Cost Query Workflow

For the full cost query workflow (scope selection, report types, timeframes, dataset configuration, API calls, pagination, guardrails, examples, and error handling), see:

๐Ÿ“„ **[Cost Query Workflow](cost-query/workflow.md)**

---

## Part 2: Cost Optimization Workflow

For the full cost optimization workflow (prerequisites, best practices, Redis/AKS-specific analysis, Azure Quick Review, resource discovery, cost queries, pricing validation, utilization metrics, and report generation), see:

๐Ÿ“„ **[Cost Optimization Workflow](cost-optimization/workflow.md)**

---

## Part 3: Cost Forecast Workflow

For the full cost forecast workflow (scope selection, time period rules, dataset configuration, forecast-specific options, API calls, response interpretation, guardrails, and error handling), see:

๐Ÿ“„ **[Cost Forecast Workflow](cost-forecast/workflow.md)**

---

## Data Classification

- **ACTUAL DATA** = Retrieved from Azure Cost Management API
- **ACTUAL METRICS** = Retrieved from Azure Monitor
- **VALIDATED PRICING** = Retrieved from official Azure pricing pages
- **ESTIMATED SAVINGS** = Calculated based on actual data and validated pricing

## Best Practices

- Always query actual costs first โ€” never estimate or assume
- Always present the total bill alongside optimization recommendations
- Validate pricing from official sources โ€” account for free tiers
- Use REST API for cost queries (more reliable than `az costmanagement query`)
- Save audit trail โ€” include all queries and responses
- Include Azure Portal links for all resources
- For costs < $10/month, emphasize operational improvements over financial savings
- Never execute destructive operations without explicit approval

## Common Pitfalls

- **Assuming costs**: Always query actual data from Cost Management API
- **Ignoring free tiers**: Many services have generous allowances
- **Using wrong date ranges**: 30 days for costs, 14 days for utilization
- **Not showing the bill**: Always present cost breakdown alongside optimization recommendations
- **Cost query failures**: Use `az rest` with JSON body, not `az costmanagement query`

## Safety Requirements

- Get approval before deleting resources
- Test changes in non-production first
- Provide dry-run commands for validation
- Include rollback procedures

## SDK Quick References

- **Redis Management**: [.NET](cost-optimization/sdk/azure-resource-manager-redis-dotnet.md)
cost-forecast/
error-handling.md 4.1 KB
# Forecast API Error Handling

## HTTP Status Codes

| Status | Error | Cause | Remediation |
|---|---|---|---|
| 400 | Bad Request | Invalid request body, missing `dataset`, past-only dates, invalid field dependency combinations | Check request body structure; ensure `to` date is in the future; verify `includeActualCost`/`includeFreshPartialCost` dependency |
| 401 | Unauthorized | Authentication failure โ€” missing or expired token | Re-authenticate with `az login` or refresh the access token |
| 403 | Forbidden | Insufficient permissions on the scope | Ensure the identity has **Cost Management Reader** role (or higher) on the target scope |
| 404 | Not Found | Invalid scope URL โ€” subscription, resource group, or billing account not found | Verify the scope URL path and resource IDs are correct |
| 424 | Failed Dependency | Bad training data โ€” forecast model cannot compute predictions | Falls back to actual costs if `includeActualCost=true`; otherwise suggest using **the Cost Query workflow (Part 1)** for historical data |
| 429 | Too Many Requests | Rate limited โ€” QPU quota exceeded | Read `x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after` header and wait before retrying |
| 503 | Service Unavailable | Temporary service issue | Check [Azure Status](https://status.azure.com) for service health. |

## Validation Error Reference

| Error Code | Description | Fix |
|---|---|---|
| `EmptyForecastRequestBody` | Request body is empty or null | Provide a complete request body with `type`, `timeframe`, `timePeriod`, and `dataset` |
| `InvalidForecastRequestBody` | Request body has invalid JSON structure | Check JSON syntax โ€” verify braces, commas, and field names |
| `DontContainsDataSet` | The `dataset` field is missing from the request body | Add the `dataset` object with `granularity` and `aggregation` |
| `DontContainsValidTimeRangeWhileContainsPeriod` | `timePeriod` is present but `from` or `to` is invalid | Ensure both `from` and `to` are valid ISO 8601 datetime strings |
| `DontContainsValidTimeRangeWhileMonthlyAndIncludeCost` | Monthly granularity with `includeActualCost=true` but missing valid `timePeriod` | Add explicit `timePeriod` with valid `from` and `to` dates |
| `DontContainIncludeActualCostWhileIncludeFreshPartialCost` | `includeFreshPartialCost=true` without `includeActualCost=true` | Set `includeActualCost=true` or set `includeFreshPartialCost=false` |
| `CantForecastOnThePast` | Both `from` and `to` dates are in the past | Ensure the `to` date is in the future |

## Forecast-Specific Scenarios

| Scenario | Response | Action |
|---|---|---|
| "Forecast is unavailable for the specified time period" | Valid response with null/empty rows | Not an error โ€” insufficient history (< 28 days). Suggest using **the Cost Query workflow (Part 1)** for available historical data. |
| "Can't forecast on the past" | 400 error with `CantForecastOnThePast` | Ensure the `to` date is in the future. |
| Bad training data | 424 Failed Dependency | If `includeActualCost=true`, the response falls back to actual cost data only. Otherwise, suggest using **the Cost Query workflow (Part 1)** for historical data. |
| Parsing exception | 400 Bad Request | Check JSON format โ€” validate braces, quotes, commas, and field types. |

## Retry Strategy

| Status | Retry? | Strategy |
|---|---|---|
| 429 | โœ… Yes | Wait for duration specified in `x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after` header. **Maximum 3 retries.** |
| 400 | โŒ No | Fix the request body based on the validation error code |
| 401 | โŒ No | Re-authenticate โ€” the token is missing or expired |
| 403 | โŒ No | Grant **Cost Management Reader** role on the target scope |
| 404 | โŒ No | Fix the scope URL โ€” verify subscription, resource group, or billing account IDs |
| 424 | โŒ No | Training data issue โ€” retrying will not help. Fall back to actual costs or use **the Cost Query workflow (Part 1)** |
| 503 | โŒ No | Do not retry. Check [Azure Status](https://status.azure.com) for service health. |

> โš ๏ธ **Warning:** Do not retry any errors except 429. All other errors indicate issues that must be fixed before re-attempting the request.
examples.md 3.0 KB
# Forecast API Examples

Common forecast patterns with request bodies. Use the [SKILL.md workflow](../SKILL.md) to construct and execute the `az rest` command.

## 1. Forecast Rest of Current Month (Daily)

```json
{
  "type": "ActualCost",
  "timeframe": "Custom",
  "timePeriod": {
    "from": "<first-of-month>",
    "to": "<last-of-month>"
  },
  "dataset": {
    "granularity": "Daily",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    },
    "sorting": [
      { "direction": "Ascending", "name": "UsageDate" }
    ]
  },
  "includeActualCost": true,
  "includeFreshPartialCost": true
}
```

> ๐Ÿ’ก **Tip:** Set `from` to the first of the month โ€” the response contains `Actual` rows up to today and `Forecast` rows for remaining days.

---

## 2. Forecast Next 3 Months (Monthly)

```json
{
  "type": "ActualCost",
  "timeframe": "Custom",
  "timePeriod": {
    "from": "<first-of-month>",
    "to": "<3-months-out>"
  },
  "dataset": {
    "granularity": "Monthly",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    },
    "sorting": [
      { "direction": "Ascending", "name": "BillingMonth" }
    ]
  },
  "includeActualCost": true,
  "includeFreshPartialCost": true
}
```

> ๐Ÿ’ก **Tip:** Monthly granularity uses the `BillingMonth` column in the response.

---

## 3. Forecast for Resource Group Scope

```json
{
  "type": "ActualCost",
  "timeframe": "Custom",
  "timePeriod": {
    "from": "<start-date>",
    "to": "<end-date>"
  },
  "dataset": {
    "granularity": "Daily",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    },
    "sorting": [
      { "direction": "Ascending", "name": "UsageDate" }
    ]
  },
  "includeActualCost": true,
  "includeFreshPartialCost": true
}
```

> ๐Ÿ’ก **Tip:** Scope is set at the URL level. Use the resource group scope URL to limit the forecast.

---

## 4. Forecast for Billing Account Scope

```json
{
  "type": "ActualCost",
  "timeframe": "Custom",
  "timePeriod": {
    "from": "<start-date>",
    "to": "<end-date>"
  },
  "dataset": {
    "granularity": "Monthly",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    },
    "sorting": [
      { "direction": "Ascending", "name": "BillingMonth" }
    ]
  },
  "includeActualCost": true,
  "includeFreshPartialCost": true
}
```

> ๐Ÿ’ก **Tip:** Use URL pattern `/providers/Microsoft.Billing/billingAccounts/<id>/...`. Monthly granularity recommended for billing account forecasts.

---

## Scope URL Reference

| Scope | URL Pattern |
|---|---|
| Subscription | `/subscriptions/<subscription-id>/providers/Microsoft.CostManagement/forecast` |
| Resource Group | `/subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.CostManagement/forecast` |
| Billing Account | `/providers/Microsoft.Billing/billingAccounts/<id>/providers/Microsoft.CostManagement/forecast` |

> ๐Ÿ’ก **Tip:** These are path-only patterns โ€” not complete URLs. Append `?api-version=2023-11-01` when constructing the full request URL.
guardrails.md 4.3 KB
# Forecast API Guardrails

Detailed guardrails derived from CCM-LUX getForecastData and CCM-UX-MIDDLEWARE Forecaster.

## Time Period Validation

| Rule | Detail |
|---|---|
| `to` date must be in the future | `numberOfDaysToForecast` must be > 0. Entirely past date ranges return a `CantForecastOnThePast` error. |
| `from` can be in the past | When `from` is in the past, the response includes actual costs from `from` to today and forecast costs from today to `to`. |
| Both dates must be valid | When `timePeriod` is present, both `from` and `to` must be valid parseable ISO 8601 datetime strings. |
| Monthly + includeActualCost | Monthly granularity with `includeActualCost=true` requires an explicit `timePeriod` with valid `from` and `to` dates. Omitting it produces `DontContainsValidTimeRangeWhileMonthlyAndIncludeCost`. |
| Maximum forecast period | 10 years maximum forecast window. |

> โš ๏ธ **Warning:** If both `from` and `to` are in the past, the API returns `CantForecastOnThePast`. At least the `to` date must be in the future.

## Training Data Requirements

| Requirement | Value |
|---|---|
| Minimum historical data | 4 weeks (28 days) of cost data |
| Preferred training window | Up to 3 months of history |
| Late arrival tolerance | 2 days for billing data to arrive |
| New subscriptions (< 28 days) | Forecast unavailable |

> โš ๏ธ **Warning:** New subscriptions with fewer than 28 days of cost history cannot generate forecasts. Suggest using **the Cost Query workflow (Part 1)** to retrieve available historical data instead.

## Grouping Restriction

| Aspect | Detail |
|---|---|
| Grouping support | โŒ **Not supported** |
| API limitation | This is a hard limitation of the Forecast API. The `grouping` field is not accepted in the request body. |
| Workaround | If the user requests a grouped forecast (e.g., forecast by resource group or service), inform them that grouping is not supported for forecasts. Suggest querying historical data with grouping using **the Cost Query workflow (Part 1)** instead. |

> โš ๏ธ **Warning:** Even when using **the Cost Query workflow (Part 1)** for grouped historical data, `ResourceId` grouping is only supported at subscription scope and below. It is not supported at billing account, management group, or higher scopes.

## Response Row Limit

| Constraint | Detail |
|---|---|
| Maximum rows | 40 rows per forecast response |
| Daily example | 30 actual days + 30 forecast days = 60 rows โ†’ **exceeds limit** |
| Recommendation | For daily granularity, keep forecast period to ~2โ€“3 weeks |
| Longer periods | Use monthly granularity to stay within the row limit |

> ๐Ÿ’ก **Tip:** If the user needs a daily forecast for more than 2โ€“3 weeks, consider splitting the request into smaller time windows or switching to monthly granularity.

## includeActualCost / includeFreshPartialCost

| Field | Default | Dependency |
|---|---|---|
| `includeActualCost` | `true` | None |
| `includeFreshPartialCost` | `true` | **Requires `includeActualCost=true`** |

> โš ๏ธ **Warning:** Setting `includeFreshPartialCost=true` without `includeActualCost=true` produces validation error `DontContainIncludeActualCostWhileIncludeFreshPartialCost`. Always set both fields explicitly.

## Forecast Availability

The API returns "Forecast is unavailable for the specified time period" when:

| Condition | Detail |
|---|---|
| Null/empty response rows | Response has no data rows |
| Insufficient training data | Scope has fewer than 28 days of cost history |
| No cost history | Scope has never had any cost data |

> โš ๏ธ **Warning:** This is **not an error** โ€” it is a valid response indicating the forecast model cannot generate predictions. Do not retry. Instead, suggest using **the Cost Query workflow (Part 1)** to retrieve whatever historical data is available.

## Rate Limiting

| Header | Description |
|---|---|
| `x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after` | Seconds to wait before retrying (QPU-based) |
| `x-ms-ratelimit-microsoft.costmanagement-entity-retry-after` | Seconds to wait for entity-level throttle |
| `x-ms-ratelimit-microsoft.costmanagement-tenant-retry-after` | Seconds to wait for tenant-level throttle |

The Forecast API uses the same QPU-based rate limiting as the Query API. When a 429 response is received, read the retry-after headers and wait before retrying.
request-body-schema.md 4.2 KB
# Forecast API Request Body Schema

## Complete JSON Schema

```json
{
  "type": "ActualCost",
  "timeframe": "Custom",
  "timePeriod": {
    "from": "2024-01-01T00:00:00Z",
    "to": "2024-03-31T00:00:00Z"
  },
  "dataset": {
    "granularity": "Daily",
    "aggregation": {
      "totalCost": {
        "name": "Cost",
        "function": "Sum"
      }
    },
    "sorting": [
      {
        "direction": "Ascending",
        "name": "UsageDate"
      }
    ],
    "filter": {
      "dimensions": {
        "name": "ResourceGroupName",
        "operator": "In",
        "values": ["my-resource-group"]
      }
    }
  },
  "includeActualCost": true,
  "includeFreshPartialCost": true
}
```

## Field Reference

| Field | Type | Required | Values | Description |
|---|---|---|---|---|
| `type` | string | โœ… | `ActualCost`, `AmortizedCost`, `Usage` | Cost type for the forecast |
| `timeframe` | string | โœ… | `Custom` | Must be `Custom` for forecast requests |
| `timePeriod` | object | โœ… | โ€” | Start and end dates for the forecast window |
| `timePeriod.from` | string | โœ… | ISO 8601 datetime | Start date; can be in the past to include actuals |
| `timePeriod.to` | string | โœ… | ISO 8601 datetime | End date; **must be in the future** for forecast |
| `dataset` | object | โœ… | โ€” | Dataset configuration for the forecast |
| `dataset.granularity` | string | โœ… | `Daily`, `Monthly` | Time granularity of forecast results |
| `dataset.aggregation` | object | โœ… | โ€” | Aggregation functions to apply |
| `dataset.aggregation.totalCost.name` | string | โœ… | `Cost` | Column name to aggregate |
| `dataset.aggregation.totalCost.function` | string | โœ… | `Sum` | Aggregation function |
| `dataset.sorting` | array | Optional | โ€” | Sort order for results |
| `dataset.sorting[].direction` | string | Optional | `Ascending`, `Descending` | Sort direction |
| `dataset.sorting[].name` | string | Optional | `UsageDate` | Column to sort by |
| `dataset.filter` | object | Optional | โ€” | Filter expression (dimensions/tags) |
| `includeActualCost` | boolean | Optional | `true`, `false` | Include historical actual costs alongside forecast. Default: `true` |
| `includeFreshPartialCost` | boolean | Optional | `true`, `false` | Include partial cost data for recent days. Default: `true`. **Requires `includeActualCost=true`** |

## Forecast-Specific Fields

### `includeActualCost`

- **Type:** boolean
- **Default:** `true`
- When `true`, the response includes historical actual cost rows from the `from` date up to today, alongside projected forecast rows from today to the `to` date.
- When `false`, only forecast (projected) rows are returned.

### `includeFreshPartialCost`

- **Type:** boolean
- **Default:** `true`
- When `true`, includes partial (incomplete) cost data for the most recent days where billing data is still arriving.
- โš ๏ธ **Requires `includeActualCost=true`.** Setting `includeFreshPartialCost=true` without `includeActualCost=true` produces a validation error (`DontContainIncludeActualCostWhileIncludeFreshPartialCost`).

## Response Structure

### Response Columns

| Column | Type | Description |
|---|---|---|
| `Cost` | Number | The cost amount (actual or forecasted) |
| `UsageDate` / `BillingMonth` | Datetime | The date for the cost row |
| `CostStatus` | String | Indicates whether the row is historical or projected |
| `Currency` | String | Currency code (e.g., `USD`, `EUR`) |

### CostStatus Values

| Value | Meaning |
|---|---|
| `Actual` | Historical cost data (already incurred) |
| `Forecast` | Projected future cost (model prediction) |

### Granularity and Date Column Mapping

| Granularity | Date Column |
|---|---|
| `Daily` | `UsageDate` |
| `Monthly` | `BillingMonth` |

## Key Differences from Query API Request Body

| Aspect | Forecast API | Query API |
|---|---|---|
| Grouping | โŒ Not supported | โœ… Supported via `grouping` field |
| `timeframe` | Typically `Custom` only | Supports `Custom`, `MonthToDate`, `BillingMonthToDate`, etc. |
| `includeActualCost` | โœ… Forecast-specific field | โŒ Not applicable |
| `includeFreshPartialCost` | โœ… Forecast-specific field | โŒ Not applicable |
| Response `CostStatus` column | โœ… Distinguishes `Actual` vs `Forecast` rows | โŒ Not present |
| `to` date | Must be in the future | Can be any valid past/present date |
workflow.md 4.6 KB
# Cost Forecast Workflow

Use this workflow when the user wants to **project future costs**.

> โš ๏ธ **Warning:** If the user wants **historical** cost data, use the [Cost Query Workflow](../cost-query/workflow.md). If they want to **reduce** costs, use the [Cost Optimization Workflow](../cost-optimization/workflow.md).

## Key Differences from Query API

| Aspect | Query API | Forecast API |
|--------|-----------|--------------|
| Purpose | Historical cost data | Projected future costs |
| Time period | Past dates only | Must include future dates |
| Grouping | Up to 2 dimensions | **Not supported** |
| `includeActualCost` | N/A | Include historical alongside forecast |
| Response columns | Cost, Date, Currency | Cost, Date, **CostStatus**, Currency |
| Max response rows | 5,000/page | 40 rows recommended |
| Timeframe | Multiple presets + Custom | Typically `Custom` only |

## Step 1: Determine Scope

Use the same scope patterns from the Scope Reference table in the main [SKILL.md](../SKILL.md#scope-reference-shared-across-all-workflows).

## Step 2: Choose Report Type

`ActualCost` is most common for forecasting. `AmortizedCost` for reservation/savings plan projections.

## Step 3: Set Time Period

> โš ๏ธ **Warning:** The `to` date **MUST** be in the future.

- Set `timeframe` to `Custom` and provide `timePeriod` with `from` and `to` dates
- `from` can be in the past โ€” shows actual costs up to today, then forecast to `to`
- Minimum 28 days of historical cost data required
- Maximum forecast period: 10 years

> **Full rules:** [Forecast Guardrails](./guardrails.md)

## Step 4: Configure Dataset

- **Granularity**: `Daily` or `Monthly` recommended
- **Aggregation**: Typically `Sum` of `Cost`
- See [Forecast Request Body Schema](./request-body-schema.md) for full schema

> โš ๏ธ **Warning:** Grouping is **NOT supported** for forecast. Suggest using the [Cost Query Workflow](../cost-query/workflow.md) for grouped historical data instead.

## Step 5: Set Forecast-Specific Options

| Field | Default | Description |
|-------|---------|-------------|
| `includeActualCost` | `true` | Include historical actual costs alongside forecast |
| `includeFreshPartialCost` | `true` | Include partial cost data for recent days. **Requires `includeActualCost: true`** |

## Step 6: Construct and Execute

**Create `temp/cost-forecast.json`:**
```json
{
  "type": "ActualCost",
  "timeframe": "Custom",
  "timePeriod": {
    "from": "<first-of-month>",
    "to": "<last-of-month>"
  },
  "dataset": {
    "granularity": "Daily",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    },
    "sorting": [{ "direction": "Ascending", "name": "UsageDate" }]
  },
  "includeActualCost": true,
  "includeFreshPartialCost": true
}
```

**Execute:**
```powershell
New-Item -ItemType Directory -Path "temp" -Force

az rest --method post `
  --url "/subscriptions/<subscription-id>/providers/Microsoft.CostManagement/forecast?api-version=2023-11-01" `
  --body '@temp/cost-forecast.json'
```

## Step 7: Interpret Response

| CostStatus | Meaning |
|------------|---------|
| `Actual` | Historical actual cost (when `includeActualCost: true`) |
| `Forecast` | Projected future cost |

> ๐Ÿ’ก **Tip:** "Forecast is unavailable for the specified time period" is not an error โ€” it means the scope has insufficient historical data. Suggest using the [Cost Query Workflow](../cost-query/workflow.md) for available data.

## Key Guardrails

| Rule | Constraint |
|------|-----------|
| `to` date | Must be in the future |
| Grouping | Not supported |
| Min training data | 28 days of historical cost data |
| Max forecast period | 10 years |
| Response row limit | 40 rows recommended |
| `includeFreshPartialCost` | Requires `includeActualCost: true` |
| Monthly + includeActualCost | Requires explicit `timePeriod` |

> **Full details:** [Forecast Guardrails](./guardrails.md)

## Error Handling

| Status | Error | Remediation |
|--------|-------|-------------|
| 400 | Can't forecast on the past | Ensure `to` date is in the future. |
| 400 | Missing dataset | Add required `dataset` field. |
| 400 | Invalid dependency | Set `includeActualCost: true` when using `includeFreshPartialCost`. |
| 403 | Forbidden | Needs **Cost Management Reader** role on scope. |
| 424 | Bad training data | Insufficient history; falls back to actual costs if available. |
| 429 | Rate limited | Retry after `x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after` header. **Max 3 retries.** |
| 503 | Service unavailable | Check [Azure Status](https://status.azure.com). |

> **Full details:** [Forecast Error Handling](./error-handling.md)

For more forecast examples, see [forecast examples](./examples.md).
cost-optimization/
auth-best-practices.md 6.0 KB
# Azure Authentication Best Practices

> Source: [Microsoft โ€” Passwordless connections for Azure services](https://learn.microsoft.com/azure/developer/intro/passwordless-overview) and [Azure Identity client libraries](https://learn.microsoft.com/dotnet/azure/sdk/authentication/).

## Golden Rule

Use **managed identities** and **Azure RBAC** in production. Reserve `DefaultAzureCredential` for **local development only**.

## Authentication by Environment

| Environment | Recommended Credential | Why |
|---|---|---|
| **Production (Azure-hosted)** | `ManagedIdentityCredential` (system- or user-assigned) | No secrets to manage; auto-rotated by Azure |
| **Production (on-premises)** | `ClientCertificateCredential` or `WorkloadIdentityCredential` | Deterministic; no fallback chain overhead |
| **CI/CD pipelines** | `AzurePipelinesCredential` / `WorkloadIdentityCredential` | Scoped to pipeline identity |
| **Local development** | `DefaultAzureCredential` | Chains CLI, PowerShell, and VS Code credentials for convenience |

## Why Not `DefaultAzureCredential` in Production?

1. **Unpredictable fallback chain** โ€” walks through multiple credential types, adding latency and making failures harder to diagnose.
2. **Broad surface area** โ€” checks environment variables, CLI tokens, and other sources that should not exist in production.
3. **Non-deterministic** โ€” which credential actually authenticates depends on the environment, making behavior inconsistent across deployments.
4. **Performance** โ€” each failed credential attempt adds network round-trips before falling back to the next.

## Production Patterns

### .NET

```csharp
using Azure.Identity;

var credential = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development"
    ? new DefaultAzureCredential()                          // local dev โ€” uses CLI/VS credentials
    : new ManagedIdentityCredential();                      // production โ€” deterministic, no fallback chain
// For user-assigned identity: new ManagedIdentityCredential("<client-id>")
```

### TypeScript / JavaScript

```typescript
import { DefaultAzureCredential, ManagedIdentityCredential } from "@azure/identity";

const credential = process.env.NODE_ENV === "development"
  ? new DefaultAzureCredential()                          // local dev โ€” uses CLI/VS credentials
  : new ManagedIdentityCredential();                      // production โ€” deterministic, no fallback chain
// For user-assigned identity: new ManagedIdentityCredential("<client-id>")
```

### Python

```python
import os
from azure.identity import DefaultAzureCredential, ManagedIdentityCredential

credential = (
    DefaultAzureCredential()                              # local dev โ€” uses CLI/VS credentials
    if os.getenv("AZURE_FUNCTIONS_ENVIRONMENT") == "Development"
    else ManagedIdentityCredential()                      # production โ€” deterministic, no fallback chain
)
# For user-assigned identity: ManagedIdentityCredential(client_id="<client-id>")
```

### Java

```java
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.identity.ManagedIdentityCredentialBuilder;

var credential = "Development".equals(System.getenv("AZURE_FUNCTIONS_ENVIRONMENT"))
    ? new DefaultAzureCredentialBuilder().build()          // local dev โ€” uses CLI/VS credentials
    : new ManagedIdentityCredentialBuilder().build();      // production โ€” deterministic, no fallback chain
// For user-assigned identity: new ManagedIdentityCredentialBuilder().clientId("<client-id>").build()
```

## Local Development Setup

`DefaultAzureCredential` is ideal for local dev because it automatically picks up credentials from developer tools:

1. **Azure CLI** โ€” `az login`
2. **Azure Developer CLI** โ€” `azd auth login`
3. **Azure PowerShell** โ€” `Connect-AzAccount`
4. **Visual Studio / VS Code** โ€” sign in via Azure extension

```typescript
import { DefaultAzureCredential } from "@azure/identity";

// Local development only โ€” uses CLI/PowerShell/VS Code credentials
const credential = new DefaultAzureCredential();
```

## Environment-Aware Pattern

Detect the runtime environment and select the appropriate credential. The key principle: use `DefaultAzureCredential` only when running locally, and a specific credential in production.

> **Tip:** Azure Functions sets `AZURE_FUNCTIONS_ENVIRONMENT` to `"Development"` when running locally. For App Service or containers, use any environment variable you control (e.g. `NODE_ENV`, `ASPNETCORE_ENVIRONMENT`).

```typescript
import { DefaultAzureCredential, ManagedIdentityCredential } from "@azure/identity";

function getCredential() {
  if (process.env.NODE_ENV === "development") {
    return new DefaultAzureCredential();          // picks up az login / VS Code creds
  }
  return process.env.AZURE_CLIENT_ID
    ? new ManagedIdentityCredential(process.env.AZURE_CLIENT_ID)  // user-assigned
    : new ManagedIdentityCredential();                            // system-assigned
}
```

## Security Checklist

- [ ] Use managed identity for all Azure-hosted apps
- [ ] Never hardcode credentials, connection strings, or keys
- [ ] Apply least-privilege RBAC roles at the narrowest scope
- [ ] Use `ManagedIdentityCredential` (not `DefaultAzureCredential`) in production
- [ ] Store any required secrets in Azure Key Vault
- [ ] Rotate secrets and certificates on a schedule
- [ ] Enable Microsoft Defender for Cloud on production resources

## Further Reading

- [Passwordless connections overview](https://learn.microsoft.com/azure/developer/intro/passwordless-overview)
- [Managed identities overview](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview)
- [Azure RBAC overview](https://learn.microsoft.com/azure/role-based-access-control/overview)
- [.NET authentication guide](https://learn.microsoft.com/dotnet/azure/sdk/authentication/)
- [Python identity library](https://learn.microsoft.com/python/api/overview/azure/identity-readme)
- [JavaScript identity library](https://learn.microsoft.com/javascript/api/overview/azure/identity-readme)
- [Java identity library](https://learn.microsoft.com/java/api/overview/azure/identity-readme)
azure-aks-anomalies.md 2.4 KB
# AKS Cost Anomaly Investigation

Investigate user-reported cost or utilization spikes by correlating Azure Monitor metrics, scaling events, and Cost Management data.

## Step 1 - Confirm Timeframe

Ask the user: "When did you notice the spike? (e.g., 'last Tuesday', 'between 2 AM and 4 AM yesterday')"

## Step 2 - Pull Cost Data

```bash
az rest --method post \
  --url "https://management.azure.com/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.CostManagement/query?api-version=2023-11-01" \
  --body '{
    "type": "ActualCost",
    "timeframe": "Custom",
    "timePeriod": { "from": "<start-date>", "to": "<end-date>" },
    "dataset": {
      "granularity": "Daily",
      "aggregation": { "totalCost": { "name": "Cost", "function": "Sum" } },
      "grouping": [{ "type": "Dimension", "name": "ResourceId" }]
    }
  }'
```

## Step 3 - Pull Node Count and Scaling Events

```bash
# First, verify available metrics on your AKS resource
az monitor metrics list-definitions \
  --resource "<aks-resource-id>" \
  --output table

# Node count over the anomaly window (use metric name from list-definitions output)
az monitor metrics list \
  --resource "<aks-resource-id>" \
  --metrics "<verified-node-count-metric>" \
  --interval PT5M --aggregation Count \
  --start-time "<start-date>" --end-time "<end-date>"

# HPA scaling events
kubectl get events --all-namespaces \
  --field-selector reason=SuccessfulRescale \
  --sort-by='.lastTimestamp'
```

## Step 4 - Top Consumers

```bash
kubectl top nodes
kubectl top pods --all-namespaces --sort-by=cpu
```

## Common Causes

| Symptom | Likely Cause | Action |
|---------|-------------|--------|
| Node count surged off-peak | HPA/VPA misconfiguration | Review HPA min replicas |
| Single pod consuming all CPU | Memory leak or runaway process | Check logs, add resource limits |
| Cost spike on specific day | Batch job ran unexpectedly | Review CronJob schedule |
| Persistent high node count | CAS scale-down blocked | Check PodDisruptionBudgets, system pods |
| Sudden namespace cost jump | New deployment with no resource limits | Add requests/limits |

## Set Up Budget Alert

```bash
az consumption budget create \
  --budget-name "aks-monthly-budget" \
  --amount <budget-amount> \
  --time-grain Monthly \
  --start-date "<YYYY-MM-01>" \
  --end-date "<YYYY-MM-01>" \
  --resource-group "<resource-group>" \
  --threshold 80 \
  --contact-emails "<contact-email>"
```

azure-aks-cost-addon.md 1.5 KB
# AKS Cost Analysis Add-on

Enable namespace-level cost visibility using the built-in AKS cost monitoring add-on.

## Check Status

```bash
# Check if add-on is enabled
az aks show \
  --name "<cluster-name>" --resource-group "<resource-group>" \
  --query "addonProfiles.costAnalysis" -o json

# Check cluster tier (add-on requires Standard or Premium)
az aks show \
  --name "<cluster-name>" --resource-group "<resource-group>" \
  --query "{tier:sku.tier, name:name}" -o table
```

## Enable Add-on

```bash
# Requires Standard or Premium tier
az aks update \
  --name "<cluster-name>" --resource-group "<resource-group>" \
  --enable-cost-analysis
```

## If Cluster is Free Tier

Warn user that upgrading from Free to Standard introduces an ongoing cluster management fee. Use the official AKS pricing page or this skillโ€™s pricing validation step to confirm the current cost with the user and obtain explicit approval before proceeding. After user approval:

```bash
az aks update \
  --name "<cluster-name>" --resource-group "<resource-group>" \
  --tier standard

az aks update \
  --name "<cluster-name>" --resource-group "<resource-group>" \
  --enable-cost-analysis
```

## After Enabling

Namespace-level cost data is available in:
- Azure Portal: AKS cluster -> Cost Analysis blade
- Azure Cost Management: filter by cluster resource ID + `kubernetes namespace` dimension

> Risk: Low for enabling the add-on. Upgrading tier (Free -> Standard) has a cost โ€” always confirm with user first.
azure-quick-review.md 1.6 KB
## Azure Quick Review (azqr) for Cost Optimization

Azure Quick Review (azqr) generates compliance and governance reports that identify cost-impacting issues and orphaned resources.

## Create Filters Configuration

Create a `filters.yaml` file to focus the scan on cost optimization:

```yaml
includeSections:
  - Costs
  - Advisor
  - Inventory
  - Orphaned
excludeSections:
  - Recommendations
  - AzurePolicy
  - DefenderRecommendations
```

## Run the azqr Scan

Execute the scan using Azure MCP or CLI:

```powershell
# Via Azure MCP (preferred if available)
# Use the extension_azqr tool with subscription and optional resource-group parameters

# Or via direct CLI:
azqr scan --subscription "<SUBSCRIPTION_ID>" --resource-group "<RESOURCE_GROUP>" --filters ./filters.yaml --output json
```

## Save Output

Save all generated files to the `output/` folder:
1. Create the folder: `mkdir output` (if it doesn't exist)
2. Save the azqr report as: `output/azqr_report_<YYYYMMDD_HHMMSS>.json`
3. After the scan completes, delete the temporary `filters.yaml` file

## Report Output

The scan generates a JSON report with recommendations categorized by impact level (High/Medium/Low), including:
- Orphaned resources (NICs, disks, IPs)
- Azure Advisor cost recommendations  
- Resource inventory
- Cost breakdown by resource

## Notes

- azqr provides qualitative governance recommendations
- Always validate findings with actual cost data before making changes
- The tool requires Reader role on the subscription or resource group
- Save reports to `output/` folder with timestamps for audit trail
azure-resource-graph.md 2.7 KB
# Azure Resource Graph Queries for Cost Optimization

Azure Resource Graph (ARG) enables fast, cross-subscription resource querying using KQL via `az graph query`. Use it to find orphaned resources, unused infrastructure, and optimization targets.

## How to Query

Use the `extension_cli_generate` MCP tool to generate `az graph query` commands:

```yaml
azure__extension_cli_generate
  intent: "query Azure Resource Graph to <describe what you want to find>"
  cli-type: "az"
```

Or construct directly:

```bash
az graph query -q "<KQL>" --query "data[].{name:name, type:type}" -o table
```

> โš ๏ธ **Prerequisite:** `az extension add --name resource-graph`

## Key Tables

| Table | Contains |
|-------|----------|
| `Resources` | All ARM resources (name, type, location, properties, tags) |
| `ResourceContainers` | Subscriptions, resource groups, management groups |
| `AdvisorResources` | Cost and performance recommendations |

## Cost Optimization Query Patterns

**Find orphaned (unattached) managed disks:**

```kql
Resources
| where type =~ 'microsoft.compute/disks'
| where isempty(managedBy)
| project name, resourceGroup, location, diskSizeGb=properties.diskSizeGB, sku=sku.name
```

**Find unattached public IP addresses:**

```kql
Resources
| where type =~ 'microsoft.network/publicipaddresses'
| where isempty(properties.ipConfiguration)
| project name, resourceGroup, location, sku=sku.name
```

**Find orphaned network interfaces:**

```kql
Resources
| where type =~ 'microsoft.network/networkinterfaces'
| where isempty(properties.virtualMachine)
| project name, resourceGroup, location
```

**Resource count by SKU/tier (spot oversized resources):**

```kql
Resources
| where isnotempty(sku.name)
| summarize count() by type, tostring(sku.name)
| order by count_ desc
```

**Tag coverage for cost allocation:**

```kql
Resources
| extend hasCostCenter = isnotnull(tags['CostCenter'])
| summarize total=count(), tagged=countif(hasCostCenter) by type
| extend coverage=round(100.0 * tagged / total, 1)
| order by total desc
```

**Find idle load balancers (no backend pools):**

```kql
Resources
| where type =~ 'microsoft.network/loadbalancers'
| where array_length(properties.backendAddressPools) == 0
| project name, resourceGroup, location, sku=sku.name
```

**Get Advisor cost recommendations:**

```kql
AdvisorResources
| where properties.category == 'Cost'
| project name, impact=properties.impact, description=properties.shortDescription.solution
```

## Tips

- Use `=~` for case-insensitive type matching (resource types are lowercase)
- Navigate properties with `properties.fieldName`
- Use `--first N` to limit result count
- Use `--subscriptions` to scope to specific subscriptions
- Cross-reference orphaned resources with cost data from Cost Management API
report-template.md 1.8 KB
# Cost Optimization Report Template

Use `create_file` with path `output/costoptimizereport<YYYYMMDD_HHMMSS>.md` and the following structure:

```markdown
# Azure Cost Optimization Report
**Generated**: <timestamp>

## Executive Summary
- Total Monthly Cost: $X (ACTUAL DATA from Cost Management API)
- Top Cost Drivers: [List top 3 services with costs]
- Potential Savings: $Y/month

## Cost Breakdown by Service
| Service | Cost (USD) | % of Total |
|---------|-----------|------------|
| ... | ... | ... |
| **Total** | **$X** | **100%** |

## Free Tier Analysis
[Resources operating within free tiers]

## Orphaned Resources (Immediate Savings)
[From azqr โ€” resources that can be deleted immediately]

## Optimization Recommendations

### Priority 1: High Impact, Low Risk
- ACTUAL cost: $X/month
- ESTIMATED savings: $Y/month
- Commands to execute

### Priority 2: Medium Impact, Medium Risk
- ACTUAL baseline, ACTUAL metrics, VALIDATED pricing
- ESTIMATED savings with commands

### Priority 3: Long-term Optimization
[Reserved Instances, Storage tiering]

## Total Estimated Savings
- Monthly: $X | Annual: $Y

## Implementation Commands
[Safe commands with approval warnings]

## Validation Appendix
- Cost Query Results: `output/cost-query-result<timestamp>.json`
- Pricing Sources: [Links]
```

## Portal Link Format

Include Azure Portal links for all resources using this format:

```text
https://portal.azure.com/#@<TENANT_ID>/resource/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP>/providers/<RESOURCE_PROVIDER>/<RESOURCE_TYPE>/<RESOURCE_NAME>/overview
```

## Audit Trail

Save cost query results to `output/cost-query-result<YYYYMMDD_HHMMSS>.json` for reproducibility.

## Cleanup

After generating the report, remove temporary files:

```powershell
Remove-Item -Path "temp" -Recurse -Force -ErrorAction SilentlyContinue
```
workflow.md 6.9 KB
# Cost Optimization Workflow

Use this workflow when the user wants to **reduce their costs** โ€” find waste, orphaned resources, rightsizing opportunities.

> **Important:** Always present the total bill and cost breakdown (from the [Cost Query Workflow](../cost-query/workflow.md)) alongside optimization recommendations.

## Step 0: Validate Prerequisites

**Required Tools:**
- Azure CLI installed and authenticated (`az login`)
- Azure CLI extensions: `costmanagement`, `resource-graph`
- Azure Quick Review (azqr) installed โ€” See [Azure Quick Review](./azure-quick-review.md)

**Required Permissions:**
- Cost Management Reader role
- Monitoring Reader role
- Reader role on subscription/resource group

**Verification commands:**
```powershell
az --version
az account show
az extension show --name costmanagement
azqr version
```

## Step 1: Load Best Practices

```javascript
azure__get_azure_bestpractices({
  intent: "Get cost optimization best practices",
  command: "get_bestpractices",
  parameters: { resource: "cost-optimization", action: "all" }
})
```

## Step 1.5: Redis-Specific Analysis (Conditional)

**If the user specifically requests Redis cost optimization**, use the specialized Redis reference:

**Reference**: [Azure Redis Cost Optimization](./services/redis/azure-cache-for-redis.md)

**When to use:**
- User mentions "Redis", "Azure Cache for Redis", or "Azure Managed Redis"
- Focus is on Redis resource optimization, not general subscription analysis

> ๐Ÿ’ก **Note:** For general subscription-wide optimization, continue with Step 2. For Redis-only analysis, follow the Redis reference document.

## Step 1.6: Choose Analysis Scope (for Redis-specific analysis)

**If performing Redis cost optimization**, ask the user to select:
1. **Specific Subscription ID**
2. **Subscription Name**
3. **Subscription Prefix** (e.g., "CacheTeam")
4. **All My Subscriptions**
5. **Tenant-wide**

Wait for user response before proceeding.

## Step 1.7: AKS-Specific Analysis (Conditional)

**If the user specifically requests AKS cost optimization**, use the specialized AKS reference files:

**When to use AKS-specific analysis:**
- User mentions "AKS", "Kubernetes", "cluster", "node pool", "pod", or "kubectl"
- User wants to enable the AKS cost analysis add-on or namespace cost visibility
- User reports a cost spike, unusual cluster utilization, or wants budget alerts

**Tool Selection:**
- **Prefer MCP first**: Use `azure__aks` for AKS operations (list clusters, get node pools, inspect configuration) โ€” it provides richer metadata and is consistent with AKS skill conventions in this repo
- **Fall back to CLI**: Use `az aks` and `kubectl` only when the specific operation cannot be performed via the MCP surface

**Reference files (load only what is needed for the request):**
- [Cost Analysis Add-on](./azure-aks-cost-addon.md) โ€” enable namespace-level cost visibility
- [Anomaly Investigation](./azure-aks-anomalies.md) โ€” cost spikes, scaling events, budget alerts

> **Note**: For general subscription-wide cost optimization (including AKS resource groups), continue with Step 2. For AKS-focused analysis, follow the instructions in the relevant reference file above.

## Step 1.8: Choose Analysis Scope (for AKS-specific analysis)

**If performing AKS cost optimization**, ask the user to select their analysis scope:

**Prompt the user with these options:**
1. **Specific Cluster Name** - Analyze a single AKS cluster
2. **Resource Group** - Analyze all clusters in a resource group
3. **Subscription ID** - Analyze all clusters in a subscription
4. **All My Clusters** - Scan all accessible clusters across subscriptions

Wait for user response before proceeding to Step 2.

## Step 2: Run Azure Quick Review

Run azqr to find orphaned resources (immediate cost savings):

**Reference**: [Azure Quick Review](./azure-quick-review.md)

```yaml
azure__extension_azqr
  subscription: "<SUBSCRIPTION_ID>"
  resource-group: "<RESOURCE_GROUP>"  # optional
```

**What to look for:**
- Orphaned resources: unattached disks, unused NICs, idle NAT gateways
- Over-provisioned resources: excessive retention periods, oversized SKUs
- Missing cost tags

## Step 3: Discover Resources

Use Azure Resource Graph for efficient cross-subscription resource discovery. See [Azure Resource Graph Queries](./azure-resource-graph.md) for orphaned resource detection patterns.

```powershell
az account show
az resource list --subscription "<SUBSCRIPTION_ID>" --resource-group "<RESOURCE_GROUP>"
```

## Step 4: Query Actual Costs

Get actual cost data from Azure Cost Management API (last 30 days). Use the [Cost Query Workflow](../cost-query/workflow.md) with this configuration:

**Create `temp/cost-query.json`:**
```json
{
  "type": "ActualCost",
  "timeframe": "Custom",
  "timePeriod": {
    "from": "<START_DATE>",
    "to": "<END_DATE>"
  },
  "dataset": {
    "granularity": "None",
    "aggregation": {
      "totalCost": {
        "name": "Cost",
        "function": "Sum"
      }
    },
    "grouping": [
      {
        "type": "Dimension",
        "name": "ResourceId"
      }
    ]
  }
}
```

> **Action Required**: Calculate `<START_DATE>` (30 days ago) and `<END_DATE>` (today) in ISO 8601 format.

**Execute and save results to `output/cost-query-result<timestamp>.json`.**

> ๐Ÿ’ก **Tip:** Also run a cost-by-service query (grouping by `ServiceName`) to present the total bill breakdown alongside optimization recommendations. See [examples.md](../cost-query/examples.md).

## Step 5: Validate Pricing

Fetch current pricing from official Azure pricing pages using `fetch_webpage`:

**Key services to validate:**
- Container Apps: https://azure.microsoft.com/pricing/details/container-apps/
- Virtual Machines: https://azure.microsoft.com/pricing/details/virtual-machines/
- App Service: https://azure.microsoft.com/pricing/details/app-service/
- Log Analytics: https://azure.microsoft.com/pricing/details/monitor/

> **Important**: Check for free tier allowances โ€” many Azure services have generous free limits.

## Step 6: Collect Utilization Metrics

Query Azure Monitor for utilization data (last 14 days) to support rightsizing recommendations:

```powershell
$startTime = (Get-Date).AddDays(-14).ToString("yyyy-MM-ddTHH:mm:ssZ")
$endTime = Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ"

# VM CPU utilization
az monitor metrics list `
  --resource "<RESOURCE_ID>" `
  --metric "Percentage CPU" `
  --interval PT1H `
  --aggregation Average `
  --start-time $startTime `
  --end-time $endTime
```

## Step 7: Generate Optimization Report

Generate a report to `output/costoptimizereport<YYYYMMDD_HHMMSS>.md` that includes an executive summary, cost breakdown by service, free tier analysis, orphaned resources, prioritized optimization recommendations, and implementation commands. Save cost query results to `output/cost-query-result<YYYYMMDD_HHMMSS>.json` for audit trail, then clean up temporary files.

For the complete report template, see [report-template.md](./report-template.md).
cost-optimization/sdk/
azure-resource-manager-redis-dotnet.md 1.5 KB
# Redis Management โ€” .NET SDK Quick Reference

> Condensed from **azure-resource-manager-redis-dotnet**. Full patterns
> (cache creation, firewall rules, access keys, geo-replication, patching)
> in the **azure-resource-manager-redis-dotnet** plugin skill if installed.

## Install
dotnet add package Azure.ResourceManager.Redis
dotnet add package Azure.Identity

## Quick Start

> **Auth:** `DefaultAzureCredential` is for local development. See [auth-best-practices.md](../auth-best-practices.md) for production patterns.

```csharp
using Azure.ResourceManager;
using Azure.Identity;
var armClient = new ArmClient(new DefaultAzureCredential());
```

## Best Practices
- Use `WaitUntil.Completed` for operations that must finish before proceeding
- Use `WaitUntil.Started` when you want to poll manually or run operations in parallel
- Use DefaultAzureCredential for **local development only**. In production, use ManagedIdentityCredential โ€” see [auth-best-practices.md](../auth-best-practices.md)
- Handle `RequestFailedException` for ARM API errors
- Use `CreateOrUpdateAsync` for idempotent operations
- Navigate hierarchy via `Get*` methods (e.g., `cache.GetRedisFirewallRules()`)
- Use Premium SKU for production workloads requiring geo-replication, clustering, or persistence
- Enable TLS 1.2 minimum โ€” set `MinimumTlsVersion = RedisTlsVersion.Tls1_2`
- Disable non-SSL port โ€” set `EnableNonSslPort = false` for security
- Rotate keys regularly โ€” use `RegenerateKeyAsync` and update connection strings
cost-optimization/services/redis/
azure-cache-for-redis.md 2.7 KB
## Azure Redis Cost Optimization

Reference guide for identifying cost savings opportunities in Azure Redis deployments through analysis and targeted scans.

## Subscription Input Options

Accept any of these identifiers to identify subscriptions for analysis:

| Input Type | Example | Use Case |
|------------|---------|----------|
| **Subscription ID** | `a1b2c3d4-...` | Analyze specific subscription |
| **Subscription Name** | `Production-Environment` | User-friendly identifier |
| **Subscription Prefix** | `CacheTeam -` | Analyze all team subscriptions |
| **Tenant ID** | `tenant-guid` | Analyze entire organization |
| **"All my subscriptions"** | (keyword) | Scan all accessible subscriptions |

## Cost Optimization Rules

When analyzing each cache, apply these prioritized rules:

| Priority | Rule | Detection Logic | Recommendation | Avg Savings |
|----------|------|----------------|----------------|-------------|
| ๐Ÿ”ด Critical | Failed Cache | `provisioningState == 'Failed'` | Delete immediately | $50-300/mo |
| ๐Ÿ”ด Critical | Stuck Creating | `provisioningState == 'Creating'` AND age >4 hours | Delete/support ticket | $50-300/mo |
| ๐ŸŸ  High | Premium in Dev | `sku.name == 'Premium'` AND `tags.environment in ['dev','test','staging']` | Downgrade to Standard | $175/mo |
| ๐ŸŸ  High | Enterprise Unused | `sku.name startsWith 'Enterprise'` AND no modules/clustering | Downgrade to Premium/Standard | $300-1000/mo |
| ๐ŸŸ  High | Old Test Cache | `tags.purpose == 'test'` AND age >60 days | Delete or downgrade | $50-150/mo |
| ๐ŸŸก Medium | Large Dev Cache | `sku.capacity >3` AND `tags.environment == 'dev'` | Reduce size | $100-300/mo |
| ๐ŸŸก Medium | No Expiration Tag | Missing `expirationDate` or `ttl` tag | Add cleanup policy | N/A |
| ๐ŸŸข Low | Untagged Resource | Missing required tags (`environment`, `owner`) | Apply tags | N/A |
| ๐ŸŸข Low | Old Cache | Age >365 days | Review if still needed | Variable |

## Report Templates

### Subscription-Level Summary
Quick overview of costs and issues per subscription (use for multi-subscription scans). Include: subscription name/ID, total monthly cost, number of caches, cache count by SKU tier, and top issues found.

### Detailed Cache Analysis
Individual cache breakdown with specific recommendations. Include: cache name, resource group, SKU tier, current cost, memory usage %, CPU usage %, connection count, and specific rightsizing recommendations.

## Tools & Commands

**MCP Tool:** `azure__redis` with command `redis_list` (parameter: `subscription`)

**Azure CLI Equivalents:**
- `az account list` - List subscriptions
- `az redis list --subscription <id>` - List Redis caches
- `az redis show` - Get cache details
- `az redis delete` - Remove cache
cost-query/
dimensions-by-scope.md 7.2 KB
# Dimensions by Scope

Dimension availability matrix for the Cost Management Query API, organized by scope type and agreement type.

## Scope URL Patterns

| Scope | URL Pattern |
|-------|-------------|
| Subscription | `/subscriptions/<subscription-id>` |
| Resource Group | `/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>` |
| Management Group | `/providers/Microsoft.Management/managementGroups/<management-group-id>` |
| Billing Account | `/providers/Microsoft.Billing/billingAccounts/<billing-account-id>` |
| Billing Profile | `/providers/Microsoft.Billing/billingAccounts/<billing-account-id>/billingProfiles/<billing-profile-id>` |
| Invoice Section | `/providers/Microsoft.Billing/billingAccounts/<billing-account-id>/billingProfiles/<billing-profile-id>/invoiceSections/<invoice-section-id>` |
| Department (EA) | `/providers/Microsoft.Billing/billingAccounts/<billing-account-id>/departments/<department-id>` |
| Enrollment Account (EA) | `/providers/Microsoft.Billing/billingAccounts/<billing-account-id>/enrollmentAccounts/<enrollment-account-id>` |
| Customer (Partner) | `/providers/Microsoft.Billing/billingAccounts/<billing-account-id>/customers/<customer-id>` |

## Common Dimensions

The following dimensions are available across most scopes:

> โš ๏ธ **Warning:** `ResourceId` grouping is **only supported at subscription and resource group scopes**. At higher scopes (billing account, management group, billing profile, etc.), use `ServiceName`, `SubscriptionName`, or another supported dimension instead. See [guardrails.md](guardrails.md) for the full scope restriction table.

| Dimension | Description |
|-----------|-------------|
| `ResourceGroupName` | Resource group containing the resource. |
| `ResourceId` | Full Azure resource ID. |
| `ResourceLocation` | Azure region where the resource is deployed. |
| `ServiceName` | Azure service name (e.g., Virtual Machines, Storage). |
| `ServiceFamily` | Service family grouping (e.g., Compute, Storage, Networking). |
| `ServiceTier` | Service tier or SKU tier (e.g., Standard, Premium). |
| `MeterCategory` | Top-level meter classification. |
| `MeterSubCategory` | Meter sub-classification. |
| `Meter` | Specific meter name. |
| `ChargeType` | Type of charge (e.g., Usage, Purchase, Refund). |
| `PublisherType` | Publisher type (e.g., Azure, Marketplace, AWS). |
| `PricingModel` | Pricing model (e.g., OnDemand, Reservation, SavingsPlan, Spot). |
| `SubscriptionName` | Subscription display name. |
| `SubscriptionId` | Subscription GUID. |
| `TagKey` | Azure resource tag key (use with `TagKey` column type in grouping). |
| `Product` | Product name from the price sheet. |
| `BenefitName` | Reservation or savings plan name. |
| `BillingPeriod` | Billing period identifier. |

## Scope-Specific Dimensions

Additional dimensions available only at certain scopes:

| Scope | Additional Dimensions |
|-------|-----------------------|
| Subscription | _(common dimensions only)_ |
| Resource Group | _(common dimensions only)_ |
| Management Group | `DepartmentName`, `EnrollmentAccountName` |
| Billing Account | `BillingProfileName`, `DepartmentName`, `EnrollmentAccountName`, `InvoiceSectionName`, `Customer` |
| Billing Profile | `InvoiceSectionName`, `Customer` |
| Invoice Section | _(common dimensions only)_ |
| Department (EA) | `EnrollmentAccountName` |
| Enrollment Account (EA) | _(common dimensions only)_ |
| Customer (Partner) | _(common dimensions only)_ |

## Agreement Type Dimension Sets

Available dimensions vary by agreement type. Only dimensions listed for your agreement type are valid in GroupBy and Filter expressions.

### EA (Enterprise Agreement)

| Dimension | Available |
|-----------|-----------|
| `ResourceGroupName` | โœ… |
| `ResourceId` | โœ… โš ๏ธ |
| `SubscriptionName` | โœ… |
| `Product` | โœ… |
| `ResourceLocation` | โœ… |
| `ServiceName` | โœ… |
| `ServiceFamily` | โœ… |
| `TagKey` | โœ… |
| `MeterSubCategory` | โœ… |
| `PublisherType` | โœ… |
| `PricingModel` | โœ… |
| `ChargeType` | โœ… |
| `ServiceTier` | โœ… |
| `BenefitName` | โœ… |
| `BillingProfileName` | โœ… |
| `DepartmentName` | โœ… |
| `EnrollmentAccountName` | โœ… |
| `BillingPeriod` | โœ… |

### MCA (Microsoft Customer Agreement)

| Dimension | Available |
|-----------|-----------|
| `ResourceGroupName` | โœ… |
| `ResourceId` | โœ… โš ๏ธ |
| `SubscriptionName` | โœ… |
| `Product` | โœ… |
| `ResourceLocation` | โœ… |
| `ServiceName` | โœ… |
| `ServiceFamily` | โœ… |
| `TagKey` | โœ… |
| `MeterSubCategory` | โœ… |
| `PublisherType` | โœ… |
| `PricingModel` | โœ… |
| `ChargeType` | โœ… |
| `ServiceTier` | โœ… |
| `BenefitName` | โœ… |
| `BillingProfileName` | โœ… |
| `InvoiceSectionName` | โœ… |

### MOSP (Microsoft Online Services Program / Pay-As-You-Go)

| Dimension | Available |
|-----------|-----------|
| `ResourceGroupName` | โœ… |
| `ResourceId` | โœ… โš ๏ธ |
| `SubscriptionName` | โœ… |
| `Product` | โœ… |
| `ResourceLocation` | โœ… |
| `ServiceName` | โœ… |
| `ServiceFamily` | โœ… |
| `TagKey` | โœ… |
| `MeterSubCategory` | โœ… |
| `PublisherType` | โœ… |
| `PricingModel` | โœ… |
| `ChargeType` | โœ… |
| `ServiceTier` | โœ… |
| `BenefitName` | โœ… |

> โš ๏ธ `ResourceId` is available as a dimension but **only works in GroupBy at subscription and resource group scopes**. At billing account or management group scopes, use `ServiceName` or `SubscriptionName` instead.

### Comparison Summary

| Dimension | EA | MCA | MOSP |
|-----------|----|----|------|
| `ResourceGroupName` | โœ… | โœ… | โœ… |
| `ResourceId` | โœ… โš ๏ธ | โœ… โš ๏ธ | โœ… โš ๏ธ |
| `SubscriptionName` | โœ… | โœ… | โœ… |
| `Product` | โœ… | โœ… | โœ… |
| `ResourceLocation` | โœ… | โœ… | โœ… |
| `ServiceName` | โœ… | โœ… | โœ… |
| `ServiceFamily` | โœ… | โœ… | โœ… |
| `TagKey` | โœ… | โœ… | โœ… |
| `MeterSubCategory` | โœ… | โœ… | โœ… |
| `PublisherType` | โœ… | โœ… | โœ… |
| `PricingModel` | โœ… | โœ… | โœ… |
| `ChargeType` | โœ… | โœ… | โœ… |
| `ServiceTier` | โœ… | โœ… | โœ… |
| `BenefitName` | โœ… | โœ… | โœ… |
| `BillingProfileName` | โœ… | โœ… | โŒ |
| `DepartmentName` | โœ… | โŒ | โŒ |
| `EnrollmentAccountName` | โœ… | โŒ | โŒ |
| `InvoiceSectionName` | โŒ | โœ… | โŒ |
| `BillingPeriod` | โœ… | โŒ | โŒ |

## Scope Resolution Priority

When multiple scope identifiers are available in context, use the following priority order (highest first):

| Priority | Scope | Notes |
|----------|-------|-------|
| 1 | Management Group | Broadest organizational scope. |
| 2 | Resource Group | Narrowest resource scope. |
| 3 | Subscription | Default scope for most queries. |
| 4 | Billing Profile + Invoice Section | MCA billing hierarchy. |
| 5 | Billing Profile | MCA billing scope. |
| 6 | Department | EA organizational unit. |
| 7 | Enrollment Account | EA enrollment scope. |
| 8 | Customer | Partner/CSP customer scope. |
| 9 | Invoice Section | Standalone invoice section. |
| 10 | Billing Account | Top-level billing scope. |

> ๐Ÿ’ก **Tip:** When a user provides both a subscription and a resource group, prefer the Resource Group scope (priority 2) over Subscription (priority 3) for more targeted results.

## Required Context Variables

The following context variables are needed to resolve scope and validate dimensions:

| Variable | Description | Used For |
|----------|-------------|----------|
| `AgreementType` | The agreement type (`EA`, `MCA`, `MOSP`). | Determines valid dimension set. |
| `AccountType` | Account type within the agreement. | Refines scope-specific behavior. |
| `CallScopeId` | The fully qualified scope URL for the API call. | Builds the request URL path. |
error-handling.md 4.9 KB
# Cost Query Error Handling

Detailed error handling reference for the Cost Management Query API.

## HTTP Status Codes

| Status | Error | Cause | Remediation |
|--------|-------|-------|-------------|
| 400 | `BadRequest` | Invalid request body, unsupported dimension, date range exceeds limits, malformed filter expression. | Validate request body against [request-body-schema.md](request-body-schema.md). Check dimension compatibility in [dimensions-by-scope.md](dimensions-by-scope.md). |
| 401 | `Unauthorized` | Missing or expired authentication token. | Re-authenticate with `az login` or refresh the bearer token. |
| 403 | `Forbidden` | Insufficient permissions on the target scope. User lacks Cost Management Reader or equivalent role. | Assign `Cost Management Reader` or `Cost Management Contributor` role on the scope. |
| 404 | `NotFound` | Scope does not exist, subscription not found, or resource group does not exist. | Verify the scope URL. Confirm the subscription ID and resource group name are correct. |
| 429 | `TooManyRequests` | Rate limit exceeded. QPU, entity, or tenant throttling triggered. | Retry after the duration specified in the `x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after` header. |
| 503 | `ServiceUnavailable` | Cost Management service is temporarily unavailable. | Check [Azure Status](https://status.azure.com) for service health. |

## Common Error Scenarios

| Error Message / Scenario | Cause | Remediation |
|--------------------------|-------|-------------|
| "Agreement type X does not support Y scope" | Scope type is incompatible with the account's agreement type. | Use a compatible scope. EA accounts cannot use Invoice Section scope; MOSP accounts cannot use Department scope. |
| "Dimension Z is not valid for scope" | The requested dimension is not available for the current scope and agreement type combination. | Check [dimensions-by-scope.md](dimensions-by-scope.md) for valid dimensions. |
| "SubscriptionName filter without SubscriptionId" | EA + Management Group scope: filtering by `SubscriptionName` without also filtering by `SubscriptionId`. | Add a `SubscriptionId` filter alongside the `SubscriptionName` filter. See [guardrails.md](guardrails.md). |
| Date range exceeds granularity limit | `Daily` range > 31 days or `Monthly`/`None` range > 12 months. | System auto-truncates `from` date. To avoid silent truncation, ensure range is within limits. |
| Date range exceeds absolute limit (37 months) | `from` to `to` spans more than 37 months. | Reduce the date range to 37 months or less. Split into multiple queries if needed. |
| "Request body is null or invalid" | Missing or malformed JSON in the request body. | Validate JSON syntax. Ensure `type`, `timeframe`, and `dataset` fields are present. |
| Invalid filter structure | `And`/`Or` has fewer than 2 child expressions, or `Not` has more than 1. | Ensure `And`/`Or` contain 2+ children. Use `Not` with exactly 1 child. For single conditions, use the filter directly without a logical wrapper. |
| "The query usage is not supported for the scope" | The query type (e.g., `AmortizedCost`) is not supported at the given scope. | Try a different scope or query type. Not all scopes support all report types. |
| `BillingSubscriptionNotFound` | The subscription ID in the scope URL is invalid or not associated with the billing account. | Verify the subscription ID exists and is active. Check that it belongs to the expected billing account. |

## Retry Strategy

| Status | Retry? | Strategy |
|--------|--------|----------|
| 429 | โœ… Yes | Wait for the duration specified in the `x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after` response header, then retry. **Maximum 3 retries.** |
| 400 | โŒ No | Fix the request. Review error message for specific field or validation issue. |
| 401 | โŒ No | Re-authenticate. Token has expired or is missing. |
| 403 | โŒ No | Fix permissions. Request appropriate RBAC role assignment on the scope. |
| 404 | โŒ No | Fix the scope URL. Verify resource exists. |
| 503 | โŒ No | Do not retry. Check [Azure Status](https://status.azure.com) for service health. |
| 5xx (other) | โŒ No | Do not retry. Investigate the error and check service health. |

> โš ๏ธ **Warning:** Do not retry any errors except 429. All other errors indicate issues that must be fixed before re-attempting the request.

## Error Response Structure

All error responses follow a consistent JSON structure:

```json
{
  "error": {
    "code": "<error-code>",
    "message": "<human-readable-error-message>",
    "details": [
      {
        "code": "<detail-code>",
        "message": "<detail-message>"
      }
    ]
  }
}
```

| Field | Description |
|-------|-------------|
| `error.code` | Machine-readable error code (e.g., `BadRequest`, `BillingSubscriptionNotFound`). |
| `error.message` | Human-readable description of the error. |
| `error.details` | Optional array of additional detail objects with more specific error information. |
examples.md 2.5 KB
# Cost Management Query Examples

Common query patterns with request bodies. Use the [SKILL.md workflow](../SKILL.md) to construct and execute the `az rest` command.

## 1. Monthly Cost by Service

```json
{
  "type": "ActualCost",
  "timeframe": "MonthToDate",
  "dataset": {
    "granularity": "None",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    },
    "grouping": [
      { "type": "Dimension", "name": "ServiceName" }
    ],
    "sorting": [
      { "direction": "Descending", "name": "Cost" }
    ]
  }
}
```

---

## 2. Daily Cost Trend (Last 30 Days)

```json
{
  "type": "ActualCost",
  "timeframe": "Custom",
  "timePeriod": {
    "from": "2024-01-01T00:00:00Z",
    "to": "2024-01-31T23:59:59Z"
  },
  "dataset": {
    "granularity": "Daily",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    }
  }
}
```

> โš ๏ธ **Warning:** Daily granularity supports a maximum of 31 days.

---

## 3. Cost by Resource Group with Tag Filter

```json
{
  "type": "ActualCost",
  "timeframe": "MonthToDate",
  "dataset": {
    "granularity": "None",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    },
    "grouping": [
      { "type": "Dimension", "name": "ResourceGroupName" }
    ],
    "filter": {
      "Tags": {
        "Name": "Environment",
        "Operator": "In",
        "Values": ["production", "staging"]
      }
    },
    "sorting": [
      { "direction": "Descending", "name": "Cost" }
    ]
  }
}
```

---

## 4. Amortized Cost for Reservation Analysis

```json
{
  "type": "AmortizedCost",
  "timeframe": "TheLastMonth",
  "dataset": {
    "granularity": "None",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    },
    "grouping": [
      { "type": "Dimension", "name": "BenefitName" }
    ],
    "sorting": [
      { "direction": "Descending", "name": "Cost" }
    ]
  }
}
```

> ๐Ÿ’ก **Tip:** `AmortizedCost` spreads reservation purchases across the term for accurate daily/monthly effective cost.

---

## 5. Top 10 Most Expensive Resources

```json
{
  "type": "ActualCost",
  "timeframe": "MonthToDate",
  "dataset": {
    "granularity": "None",
    "aggregation": {
      "totalCost": { "name": "Cost", "function": "Sum" }
    },
    "grouping": [
      { "type": "Dimension", "name": "ResourceId" }
    ],
    "sorting": [
      { "direction": "Descending", "name": "Cost" }
    ]
  }
}
```

> ๐Ÿ’ก **Tip:** Append `&$top=10` to the URL to limit results: `...query?api-version=2023-11-01&$top=10`
guardrails.md 6.6 KB
# Cost Query Guardrails

Detailed validation rules and guardrails for the Cost Management Query API. The system applies these automatically, but understanding them helps avoid unexpected query modifications or errors.

## Time Period Validation

### Default Behavior

| Scenario | System Behavior |
|----------|----------------|
| No time period specified | Defaults to current month start โ†’ today. |
| `from` is after `to` | System silently swaps `from` and `to`. |

### Future Date Handling

| Scenario | System Behavior |
|----------|----------------|
| Both `from` and `to` are in the future | Entire period shifted to the equivalent period last year. |
| Only `to` is in the future | `to` is adjusted to today's date. |

> โš ๏ธ **Warning:** Future date shifting happens silently. The response data will cover the adjusted period, not the originally requested dates.

### Granularity-Based Range Limits

| Granularity | Maximum Range | Truncation Behavior |
|-------------|---------------|---------------------|
| `Daily` | 31 days | `from` truncated to `to - 1 month + 1 day`. |
| `Monthly` | 12 months | `from` truncated to `to - 12 months + 1 day`. |
| `None` | 12 months | `from` truncated to `to - 12 months + 1 day`. |

> โš ๏ธ **Warning:** The absolute API limit is **37 months**. Requests exceeding 37 months return HTTP 400 regardless of granularity.

### Minimum Start Date

| Constraint | Value |
|------------|-------|
| Earliest allowed `from` date | May 1, 2014 |

### GroupBy Interaction with Time Period

| Combination | System Behavior |
|-------------|----------------|
| GroupBy + Daily granularity | Time period adjusted to the last day of the requested range. |
| GroupBy + Monthly granularity | Time period adjusted to the last month of the requested range. |

> ๐Ÿ’ก **Tip:** When using GroupBy with Daily granularity over a multi-day range, the system may return data only for the last day. For full daily breakdown with grouping, ensure the range is within the 31-day limit.

## ResourceId Scope Restriction

> โš ๏ธ **Warning:** Grouping by `ResourceId` is **only supported at subscription scope and below** (subscription, resource group). It is NOT supported at higher scopes.

| Scope | ResourceId GroupBy |
|-------|--------------------|
| Subscription | โœ… Supported |
| Resource Group | โœ… Supported |
| Billing Account | โŒ Not supported |
| Management Group | โŒ Not supported |
| Billing Profile | โŒ Not supported |
| Department (EA) | โŒ Not supported |
| Enrollment Account (EA) | โŒ Not supported |
| Invoice Section (MCA) | โŒ Not supported |
| Customer (Partner) | โŒ Not supported |

When the user requests a cost breakdown by resource at a billing account or management group scope, use `ServiceName`, `SubscriptionName`, or another supported dimension instead. If per-resource detail is needed, narrow the scope to a specific subscription first.

## Dataset Validation

### GroupBy Constraints

| Rule | Limit | Error Behavior |
|------|-------|----------------|
| Maximum GroupBy dimensions | 2 | Validation error if more than 2 specified. |
| Duplicate columns in GroupBy | Not allowed | Validation error on duplicate column names. |
| Same column in Aggregation and GroupBy | Not allowed | Validation error if a column appears in both. |

### Aggregation Constraints

| Rule | Details |
|------|---------|
| Standard queries aggregation function | Only `Sum` is allowed. |
| `Date` in aggregation with granularity | Not allowed. Cannot aggregate on `Date` when granularity is `Daily` or `Monthly`. |

### Filter Constraints

| Rule | Details |
|------|---------|
| `And` operator | Must have 2 or more child expressions. |
| `Or` operator | Must have 2 or more child expressions. |
| `Not` operator | Must have exactly 1 child expression. |

> โš ๏ธ **Warning:** A filter with a single child in `And` or `Or` will fail validation. Wrap single-condition filters directly without a logical operator, or use `Not` for negation.

## Scope & Dimension Compatibility

Dimensions must be valid for the intersection of the agreement type **and** scope type.

| Agreement Type | Unique Dimensions | Reference |
|----------------|-------------------|-----------|
| EA | `DepartmentName`, `EnrollmentAccountName`, `BillingPeriod` | See [dimensions-by-scope.md](dimensions-by-scope.md) |
| MCA | `InvoiceSectionName` | See [dimensions-by-scope.md](dimensions-by-scope.md) |
| MOSP | _(common dimensions only)_ | See [dimensions-by-scope.md](dimensions-by-scope.md) |

| Validation | Error Behavior |
|------------|----------------|
| Dimension not valid for agreement type | `BillingSubscriptionNotFound` or dimension validation error. |
| Dimension not valid for scope type | `BadRequest` with invalid dimension message. |

> โš ๏ธ **Warning:** Using an EA-only dimension (e.g., `DepartmentName`) on a MOSP subscription will return a validation error. Always verify the agreement type before selecting dimensions.

## EA + Management Group Special Case

| Scenario | Result |
|----------|--------|
| Filter by `SubscriptionName` without `SubscriptionId` at Management Group scope | Error returned. |
| Error message | _"To view cost data, the subscription ID is needed. Select Subscriptions to find the ID for your subscription, and then ask your question again."_ |

**Remediation:** When filtering by subscription name at Management Group scope under EA, always include a `SubscriptionId` filter alongside the `SubscriptionName` filter.

```json
{
  "And": [
    {
      "Dimensions": {
        "Name": "SubscriptionId",
        "Operator": "In",
        "Values": ["<subscription-id>"]
      }
    },
    {
      "Dimensions": {
        "Name": "SubscriptionName",
        "Operator": "In",
        "Values": ["My Subscription"]
      }
    }
  ]
}
```

## Rate Limiting

### QPU-Based Throttling

| Tier | Description |
|------|-------------|
| Premium | Higher QPU allocation (EA, MCA enterprise). |
| Non-premium | Lower QPU allocation (MOSP, trial). |

### Rate Limit Headers

| Header | Description |
|--------|-------------|
| `x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after` | Seconds to wait before retrying (QPU limit). |
| `x-ms-ratelimit-microsoft.costmanagement-entity-retry-after` | Seconds to wait before retrying (entity limit). |
| `x-ms-ratelimit-microsoft.costmanagement-tenant-retry-after` | Seconds to wait before retrying (tenant limit). |

### Pagination

| Parameter | Default | Maximum |
|-----------|---------|---------|
| Page size | 1,000 rows | 5,000 rows |
| Pagination | Use `nextLink` from response to fetch subsequent pages. | โ€” |

> ๐Ÿ’ก **Tip:** For large result sets, always check the `nextLink` field in the response. If present, make additional requests to retrieve all pages.
request-body-schema.md 5.4 KB
# Cost Management Query API โ€” Request Body Schema

Schema for the [Cost Management Query API](https://learn.microsoft.com/en-us/rest/api/cost-management/query/usage).

## Request Body Structure

```json
{
  "type": "<report-type>",
  "timeframe": "<timeframe>",
  "timePeriod": { "from": "2024-01-01T00:00:00Z", "to": "2024-01-31T23:59:59Z" },
  "dataset": {
    "granularity": "<granularity>",
    "aggregation": { "<alias>": { "name": "<column>", "function": "<function>" } },
    "grouping": [{ "type": "<column-type>", "name": "<column-name>" }],
    "filter": { "<filter-expression>" },
    "sorting": [{ "direction": "<direction>", "name": "<column>" }]
  }
}
```

## Top-Level Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | `ActualCost`, `AmortizedCost`, or `Usage` |
| `timeframe` | string | Yes | Predefined or `Custom` time window |
| `timePeriod` | object | Conditional | Required when `timeframe` is `Custom`. Contains `from`/`to` ISO 8601 dates. |
| `dataset` | object | Yes | Defines granularity, aggregation, grouping, filtering, sorting |

### Timeframe Values

`WeekToDate` ยท `MonthToDate` ยท `BillingMonthToDate` ยท `YearToDate` ยท `TheLastWeek` ยท `TheLastMonth` ยท `TheLastBillingMonth` ยท `TheLastYear` ยท `TheLast7Days` ยท `TheLast3Months` ยท `Custom`

## Dataset Fields

### Granularity

| Value | Max Range | Description |
|-------|-----------|-------------|
| `None` | 12 months | Aggregated total, no date breakdown |
| `Daily` | 31 days | Day-by-day breakdown |
| `Monthly` | 12 months | Month-by-month breakdown |

### Aggregation

```json
"aggregation": { "totalCost": { "name": "Cost", "function": "Sum" } }
```

| Field | Required | Description |
|-------|----------|-------------|
| `<alias>` (key) | Yes | Output column alias (e.g., `totalCost`) |
| `name` | Yes | Source column: `Cost`, `PreTaxCost`, or `UsageQuantity` |
| `function` | Yes | `Sum` (only supported function for cost queries) |

### Grouping

```json
"grouping": [
  { "type": "Dimension", "name": "ServiceName" },
  { "type": "TagKey", "name": "Environment" }
]
```

- `type`: `Dimension` (built-in) or `TagKey` (resource tag)
- Maximum **2** GroupBy dimensions per query. No duplicates.

### Filter

Filter expressions restrict which cost records are included. Filters support logical operators (`And`, `Or`, `Not`) and comparison operators on dimensions or tags.

#### Filter Expression Structure

```json
"filter": {
  "And": [
    {
      "Dimensions": {
        "Name": "ResourceGroupName",
        "Operator": "In",
        "Values": ["rg-prod", "rg-staging"]
      }
    },
    {
      "Not": {
        "Tags": {
          "Name": "Environment",
          "Operator": "Equal",
          "Values": ["dev"]
        }
      }
    }
  ]
}
```

#### Logical Operators

| Operator | Description | Children |
|----------|-------------|----------|
| `And` | All child expressions must match. | 2 or more expressions. |
| `Or` | Any child expression must match. | 2 or more expressions. |
| `Not` | Negates a single child expression. | Exactly 1 expression. |

> โš ๏ธ **Warning:** `And` and `Or` must contain at least 2 child expressions. `Not` must contain exactly 1.

#### Comparison Operators (ComparisonOperator Enum)

| Operator | Description | Example |
|----------|-------------|---------|
| `In` | Value is in the provided list. Supports multiple values. | `"Values": ["vm", "storage"]` |
| `Equal` | Exact match against a single value. | `"Values": ["production"]` |
| `Contains` | String contains the specified substring. | `"Values": ["prod"]` |
| `LessThan` | Numeric less-than comparison. | `"Values": ["100"]` |
| `GreaterThan` | Numeric greater-than comparison. | `"Values": ["0"]` |
| `NotEqual` | Value does not match the specified value. | `"Values": ["dev"]` |

#### Filter Target Types

| Target | Description |
|--------|-------------|
| `Dimensions` | Filter on built-in dimensions (e.g., `ResourceGroupName`, `ServiceName`). |
| `Tags` | Filter on Azure resource tags (e.g., `Environment`, `CostCenter`). |

### Sorting

```json
"sorting": [
  { "direction": "Descending", "name": "Cost" }
]
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `direction` | string | Yes | `Ascending` or `Descending`. |
| `name` | string | Yes | Column name to sort by (must be present in aggregation or grouping). |

## Response Structure

```json
{
  "id": "<query-id>",
  "name": "<query-name>",
  "type": "Microsoft.CostManagement/query",
  "properties": {
    "nextLink": "<url-for-next-page-or-null>",
    "columns": [
      { "name": "Cost", "type": "Number" },
      { "name": "ServiceName", "type": "String" },
      { "name": "UsageDate", "type": "Number" },
      { "name": "Currency", "type": "String" }
    ],
    "rows": [
      [123.45, "Virtual Machines", 20240115, "USD"],
      [67.89, "Storage", 20240115, "USD"]
    ]
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `columns` | array | Array of column definitions with `name` and `type`. |
| `columns[].name` | string | Column name. |
| `columns[].type` | string | Data type: `Number` or `String`. |
| `rows` | array | Array of row arrays. Values ordered to match `columns`. |
| `nextLink` | string | URL for next page of results, or `null` if no more pages. |

> ๐Ÿ’ก **Tip:** `UsageDate` is returned as a number in `YYYYMMDD` format (e.g., `20240115`) when granularity is `Daily` or `Monthly`.
workflow.md 4.8 KB
# Cost Query Workflow

Use this workflow when the user wants to **understand their costs** โ€” breakdowns, trends, totals, top spenders.

## Step 1: Determine Scope

Identify the Azure scope for the cost query from the Scope Reference table in the main [SKILL.md](../SKILL.md#scope-reference-shared-across-all-workflows).

## Step 2: Choose Report Type

| Type | Description |
|------|-------------|
| `ActualCost` | Actual billed costs including purchases |
| `AmortizedCost` | Reservation/savings plan costs spread across usage period |
| `Usage` | Usage-based cost data |

## Step 3: Set Timeframe

Use a preset timeframe (e.g., `MonthToDate`, `TheLastMonth`, `TheLastYear`) or `Custom` with a `timePeriod` object.

> โš ๏ธ **Warning:** Key time period guardrails:
> - **Daily granularity**: max **31 days**
> - **Monthly/None granularity**: max **12 months**
> - `Custom` timeframe **requires** a `timePeriod` object with `from` and `to` dates
> - Future dates in historical queries are silently adjusted (see guardrails for details)
>
> See [guardrails.md](./guardrails.md) for the complete set of validation rules.

## Step 4: Configure Dataset

Define granularity, aggregation, grouping, filtering, and sorting in the `dataset` object.

- **Granularity**: `None`, `Daily`, or `Monthly`
- **Aggregation**: Use `Sum` on `Cost` or `PreTaxCost` for total cost
- **Grouping**: Up to **2** `GroupBy` dimensions (e.g., `ServiceName`, `ResourceGroupName`)
- **Filtering**: Use `Dimensions` or `Tags` filters with `Name`, `Operator` (`In`, `Equal`, `Contains`), and `Values` fields
- **Sorting**: Order results by cost or dimension columns

> ๐Ÿ’ก **Tip:** Not all dimensions are available at every scope. See [dimensions-by-scope.md](./dimensions-by-scope.md) for the availability matrix.

For the full request body schema, see [request-body-schema.md](./request-body-schema.md).

## Step 5: Construct and Execute the API Call

Use `az rest` to call the Cost Management Query API.

**Create cost query file:**

Create `temp/cost-query.json` with:
```json
{
  "type": "ActualCost",
  "timeframe": "MonthToDate",
  "dataset": {
    "granularity": "None",
    "aggregation": {
      "totalCost": {
        "name": "Cost",
        "function": "Sum"
      }
    },
    "grouping": [
      {
        "type": "Dimension",
        "name": "ServiceName"
      }
    ]
  }
}
```

**Execute cost query:**
```powershell
# Create temp folder
New-Item -ItemType Directory -Path "temp" -Force

# Query using REST API (more reliable than az costmanagement query)
az rest --method post `
  --url "<scope>/providers/Microsoft.CostManagement/query?api-version=2023-11-01" `
  --body '@temp/cost-query.json'
```

## Step 6: Handle Pagination and Errors

- The API returns a maximum of **5,000 rows** per page (default: 1,000).
- If `nextLink` is present in the response, follow it to retrieve additional pages.
- Handle rate limiting (HTTP 429) by respecting `Retry-After` headers.

See [error-handling.md](./error-handling.md) for the full error reference.

## Key Guardrails

| Rule | Constraint |
|------|-----------|
| Daily granularity max range | 31 days |
| Monthly/None granularity max range | 12 months |
| Absolute API max range | 37 months |
| Max GroupBy dimensions | 2 |
| ResourceId grouping scope | Subscription and resource group only โ€” not supported at billing account, management group, or higher scopes |
| Max rows per page | 5,000 |
| Custom timeframe | Requires `timePeriod` with `from`/`to` |
| Filter AND/OR | Must have at least 2 expressions |

## Examples

**Cost by service for the current month:**

```powershell
az rest --method post `
  --url "/subscriptions/<subscription-id>/providers/Microsoft.CostManagement/query?api-version=2023-11-01" `
  --body '{
    "type": "ActualCost",
    "timeframe": "MonthToDate",
    "dataset": {
      "granularity": "None",
      "aggregation": {
        "totalCost": { "name": "Cost", "function": "Sum" }
      },
      "grouping": [
        { "type": "Dimension", "name": "ServiceName" }
      ]
    }
  }'
```

For more examples including daily trends, tag-based filtering, and multi-dimension queries, see [examples.md](./examples.md).

## Error Handling

| HTTP Status | Error | Remediation |
|-------------|-------|-------------|
| 400 | Invalid request body | Check schema, date ranges, and dimension compatibility. |
| 401 | Unauthorized | Verify authentication (`az login`). |
| 403 | Forbidden | Ensure Cost Management Reader role on scope. |
| 404 | Scope not found | Verify scope URL and resource IDs. |
| 429 | Too many requests | Retry after `x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after` header. **Max 3 retries.** |
| 503 | Service unavailable | Check [Azure Status](https://status.azure.com). |

See [error-handling.md](./error-handling.md) for detailed error handling including rate limit headers and retry strategies.

License (MIT)

View full license text
MIT License

Copyright 2025 (c) Microsoft Corporation.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.