Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
77a75b1
Add FOCUS 1.3 ingestion (#2124)
flanakin May 6, 2026
f70c43d
Add FOCUS 1.3 hub functions (#2127)
flanakin May 6, 2026
2bfa611
Add ContractCommitment dataset (#2129)
flanakin May 6, 2026
e489389
Add FOCUS 1.4-preview ingestion (#2131)
flanakin May 6, 2026
3a68afd
Add FOCUS 1.4-preview hub functions (#2133)
flanakin May 6, 2026
657aa7d
Refresh Claude Code plugin skill files for FOCUS 1.3 / 1.4 (#2119)
flanakin May 6, 2026
f090461
Add FOCUS schema tests and changelog entries (#2120)
flanakin May 6, 2026
c56b616
Add FOCUS 1.4 ingestion (IngestionSetup_v1_4.kql)
flanakin May 17, 2026
7d9d295
Merge branch 'flanakin/focus14-phase1-ingestion-1.3' into flanakin/fo…
flanakin May 17, 2026
8a01133
Add FOCUS 1.4 hub functions (HubSetup_v1_4.kql)
flanakin May 17, 2026
58f50cd
Refresh Claude Code plugin skill files for FOCUS 1.4
flanakin May 17, 2026
eba731f
Add FOCUS 1.4 schema tests and changelog entries
flanakin May 17, 2026
a681e73
chore: Update ms.date in docs-mslearn files
github-actions[bot] May 17, 2026
f8495a5
Address PR #2126 feedback
flanakin May 26, 2026
6f2ec90
Merge branch 'flanakin/focus14-phase1-ingestion-1.3' into flanakin/fo…
flanakin May 26, 2026
56f9b30
Cascade PR #2126 feedback to hub functions
flanakin May 26, 2026
676da5e
Merge branch 'flanakin/focus14-phase2-hubs-1.3' into flanakin/focus14…
flanakin May 26, 2026
cb93641
Cascade PR #2126 feedback to plugin docs
flanakin May 26, 2026
f9aa923
Merge branch 'flanakin/focus14-phase6-plugin' into flanakin/focus14-p…
flanakin May 26, 2026
3ebb967
Remove leftover singular ContractCommitment() alias
flanakin May 27, 2026
07a8850
Merge branch 'flanakin/focus14-phase6-plugin' into flanakin/focus14-p…
flanakin May 27, 2026
362de44
Cascade PR #2126 feedback to tests + changelog
flanakin May 27, 2026
bbb7180
chore: Update ms.date in docs-mslearn files
github-actions[bot] May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion docs-mslearn/toolkit/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: FinOps toolkit changelog
description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more.
author: MSBrett
ms.author: brettwil
ms.date: 04/29/2026
ms.date: 05/27/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
Expand Down Expand Up @@ -31,6 +31,17 @@ The following section lists features and enhancements that are currently in deve
- Added Claude Code plugin with skills for FinOps hubs and Azure Cost Management ([#2043](https://github.com/microsoft/finops-toolkit/pull/2043)).
- Added 4 agents (CFO, FinOps practitioner, database query, hubs agent), 5 commands (`/ftk-hubs-connect`, `/ftk-hubs-healthCheck`, `/ftk-mom-report`, `/ftk-ytd-report`, `/ftk-cost-optimization`), and an output style.
- Linked to the existing KQL query catalog in `src/queries/` from the plugin.
- **Changed**
- Updated plugin skill files to reflect FOCUS 1.4 hub schema, the new `ContractCommitments()`, `BillingPeriods()`, and `InvoiceDetails()` functions, and the removal of `ProviderName` / `PublisherName` ([#2120](https://github.com/microsoft/finops-toolkit/issues/2120)).

### FinOps hubs v15.0.0

- **Added**
- Added FOCUS 1.4 hub schema (`v1_4`). New cost and usage columns: `AllocatedMethodId`, `AllocatedMethodDetails`, `AllocatedResourceId`, `AllocatedResourceName`, `AllocatedTags`, `ContractApplied`, `ServiceProviderName`, `HostProviderName`, `CommitmentProgramEligibilityDetails`, `InvoiceDetailId`, and 12 `ContractCommitment*` per-row columns (`ContractCommitmentBenefitCategory`, `ContractCommitmentCreated`, `ContractCommitmentDiscountPercentage`, `ContractCommitmentDurationType`, `ContractCommitmentFulfillmentInterval`, `ContractCommitmentLastUpdated`, `ContractCommitmentLifecycleStatus`, `ContractCommitmentModel`, `ContractCommitmentOfferCategory`, `ContractCommitmentPaymentInterval`, `ContractCommitmentPaymentModel`, `ContractCommitmentPaymentUpfrontPercentage`). Removes deprecated `ProviderName` and `PublisherName` from the schema (raw tables keep them for back compat). Three new supplemental datasets: `ContractCommitments` (28 columns), `BillingPeriods` (6 columns), and `InvoiceDetails` (22 columns) ([#2120](https://github.com/microsoft/finops-toolkit/issues/2120)).
- Added unversioned `ContractCommitments()`, `BillingPeriods()`, and `InvoiceDetails()` functions aliasing to their `_v1_4` counterparts.
- **Changed**
- Retargeted unversioned `Costs()`, `Prices()`, `CommitmentDiscountUsage()`, `Recommendations()`, and `Transactions()` aliases to their `_v1_4` counterparts.
- Refreshed the canonical "add a new FOCUS version" procedure in [src/templates/finops-hub/docs/README.md](https://github.com/microsoft/finops-toolkit/tree/dev/src/templates/finops-hub/docs/README.md) with multi-version-cycle, plugin, and changelog steps.

### Bicep Registry module pending updates

Expand Down
266 changes: 266 additions & 0 deletions src/powershell/Tests/Unit/HubsFocusSchemas.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'HubsFocusSchemas' {

BeforeAll {
$repoRoot = (Resolve-Path "$PSScriptRoot/../../../..").Path
$scriptsPath = Join-Path $repoRoot 'src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts'
$rawTablesContent = Get-Content -Path (Join-Path $scriptsPath 'IngestionSetup_RawTables.kql') -Raw

$ingestionFiles = @{
v1_0 = Get-Content -Path (Join-Path $scriptsPath 'IngestionSetup_v1_0.kql') -Raw
v1_2 = Get-Content -Path (Join-Path $scriptsPath 'IngestionSetup_v1_2.kql') -Raw
v1_4 = Get-Content -Path (Join-Path $scriptsPath 'IngestionSetup_v1_4.kql') -Raw
}

$hubFiles = @{
v1_0 = Get-Content -Path (Join-Path $scriptsPath 'HubSetup_v1_0.kql') -Raw
v1_2 = Get-Content -Path (Join-Path $scriptsPath 'HubSetup_v1_2.kql') -Raw
v1_4 = Get-Content -Path (Join-Path $scriptsPath 'HubSetup_v1_4.kql') -Raw
Latest = Get-Content -Path (Join-Path $scriptsPath 'HubSetup_Latest.kql') -Raw
}

$appBicep = Get-Content -Path (Join-Path $repoRoot 'src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/app.bicep') -Raw
$buildConfig = Get-Content -Path (Join-Path $repoRoot 'src/templates/finops-hub/.build.config') -Raw
}

Context 'FOCUS 1.4 columns in Costs_raw' {

BeforeAll {
# Extract just the Costs_raw alter block (not Costs_final or any other table).
$script:costsRawBlock = if ($rawTablesContent -match '(?ms)\.alter table Costs_raw \(\r?\n(.*?)\r?\n\)') { $Matches[1] } else { '' }
}

It 'Costs_raw block was extracted' {
$costsRawBlock | Should -Not -BeNullOrEmpty
}

It 'Adds <_> to Costs_raw' -ForEach @(
'AllocatedMethodId', 'AllocatedMethodDetails', 'AllocatedResourceId',
'AllocatedResourceName', 'AllocatedTags', 'ContractApplied',
'ServiceProviderName', 'HostProviderName',
'CommitmentProgramEligibilityDetails', 'InvoiceDetailId',
'ContractCommitmentBenefitCategory', 'ContractCommitmentCreated',
'ContractCommitmentDiscountPercentage', 'ContractCommitmentDurationType',
'ContractCommitmentFulfillmentInterval', 'ContractCommitmentLastUpdated',
'ContractCommitmentLifecycleStatus', 'ContractCommitmentModel',
'ContractCommitmentOfferCategory', 'ContractCommitmentPaymentInterval',
'ContractCommitmentPaymentModel', 'ContractCommitmentPaymentUpfrontPercentage'
) {
$costsRawBlock | Should -Match "(?m)^\s+$_\s*:"
}

It 'Keeps deprecated <_> for back compat' -ForEach @(
'ProviderName', 'PublisherName', 'Region'
) {
$costsRawBlock | Should -Match "(?m)^\s+$_\s*:"
}
}

Context 'ContractCommitments_raw exists with all FOCUS 1.4 columns' {

BeforeAll {
# The Redefine-all-columns alter-table block is the second occurrence; match all and pick it.
$allBlocks = [regex]::Matches($rawTablesContent, '(?ms)\.alter table ContractCommitments_raw \(\r?\n(.*?)\r?\n\)')
$script:contractCommitmentsRawBlock = if ($allBlocks.Count -ge 1) { $allBlocks[$allBlocks.Count - 1].Groups[1].Value } else { '' }
}

It 'Defines ContractCommitments_raw (plural)' {
$rawTablesContent | Should -Match '\.alter table ContractCommitments_raw \('
}

It 'Does NOT define singular ContractCommitment_raw' {
$rawTablesContent | Should -Not -Match '\.alter table ContractCommitment_raw \('
}

It 'ContractCommitments_raw column block was extracted' {
$contractCommitmentsRawBlock | Should -Not -BeNullOrEmpty
}

It 'Includes base column <_>' -ForEach @(
'BillingCurrency', 'ContractCommitmentCategory', 'ContractCommitmentCost',
'ContractCommitmentId', 'ContractCommitmentPeriodEnd', 'ContractCommitmentPeriodStart',
'ContractCommitmentQuantity', 'ContractCommitmentType', 'ContractCommitmentUnit',
'ContractId', 'ContractPeriodEnd', 'ContractPeriodStart', 'InvoiceIssuerName',
'PricingCurrency'
) {
$contractCommitmentsRawBlock | Should -Match "(?m)^\s+$_\s*:"
}

It 'Includes FOCUS 1.4 column <_>' -ForEach @(
'BenefitCategory', 'ContractCommitmentApplicability', 'Created',
'DiscountPercentage', 'DurationType', 'FulfillmentInterval', 'LastUpdated',
'LifecycleStatus', 'Model', 'OfferCategory', 'PaymentInterval', 'PaymentModel',
'PaymentUpfrontPercentage', 'PricingCurrencyContractCommitmentCost'
) {
$contractCommitmentsRawBlock | Should -Match "(?m)^\s+$_\s*:"
}
}

Context 'BillingPeriods_raw exists with FOCUS 1.4 columns' {

BeforeAll {
$allBlocks = [regex]::Matches($rawTablesContent, '(?ms)\.alter table BillingPeriods_raw \(\r?\n(.*?)\r?\n\)')
$script:billingPeriodsRawBlock = if ($allBlocks.Count -ge 1) { $allBlocks[$allBlocks.Count - 1].Groups[1].Value } else { '' }
}

It 'Defines BillingPeriods_raw' {
$rawTablesContent | Should -Match '\.alter table BillingPeriods_raw \('
}

It 'Includes column <_>' -ForEach @(
'BillingPeriodCreated', 'BillingPeriodEnd', 'BillingPeriodLastUpdated',
'BillingPeriodStart', 'BillingPeriodStatus', 'InvoiceIssuerName'
) {
$billingPeriodsRawBlock | Should -Match "(?m)^\s+$_\s*:"
}
}

Context 'InvoiceDetails_raw exists with FOCUS 1.4 columns' {

BeforeAll {
$allBlocks = [regex]::Matches($rawTablesContent, '(?ms)\.alter table InvoiceDetails_raw \(\r?\n(.*?)\r?\n\)')
$script:invoiceDetailsRawBlock = if ($allBlocks.Count -ge 1) { $allBlocks[$allBlocks.Count - 1].Groups[1].Value } else { '' }
}

It 'Defines InvoiceDetails_raw' {
$rawTablesContent | Should -Match '\.alter table InvoiceDetails_raw \('
}

It 'Includes column <_>' -ForEach @(
'BilledCost', 'BillingAccountId', 'BillingCurrency', 'BillingPeriodEnd',
'BillingPeriodStart', 'ChargeCategory', 'InvoiceDetailCreated', 'InvoiceDetailDescription',
'InvoiceDetailGrain', 'InvoiceDetailId', 'InvoiceDetailLastUpdated', 'InvoiceId',
'InvoiceIssueDate', 'InvoiceIssueStatus', 'InvoiceIssuerName', 'PaymentCurrency',
'PaymentCurrencyBilledCost', 'PaymentCurrencyInvoiceDetailId', 'PaymentDueDate',
'PaymentTerms', 'PurchaseOrderNumber', 'ReferenceInvoiceId'
) {
$invoiceDetailsRawBlock | Should -Match "(?m)^\s+$_\s*:"
}
}

Context 'IngestionSetup_v1_4.kql' {

BeforeAll {
$script:costsFinalV14Block = if ($ingestionFiles.v1_4 -match '(?ms)\.create-merge table Costs_final_v1_4 \(\r?\n(.*?)\r?\n\)') { $Matches[1] } else { '' }
$script:contractCommitmentsFinalV14Block = if ($ingestionFiles.v1_4 -match '(?ms)\.create-merge table ContractCommitments_final_v1_4 \(\r?\n(.*?)\r?\n\)') { $Matches[1] } else { '' }
}

It 'Defines Costs_transform_v1_4' {
$ingestionFiles.v1_4 | Should -Match 'Costs_transform_v1_4\(\)'
}

It 'Defines Costs_final_v1_4 table' {
$ingestionFiles.v1_4 | Should -Match '\.create-merge table Costs_final_v1_4'
}

It 'Defines ContractCommitments_transform_v1_4 (plural)' {
$ingestionFiles.v1_4 | Should -Match 'ContractCommitments_transform_v1_4\(\)'
}

It 'Defines ContractCommitments_final_v1_4 table (plural)' {
$ingestionFiles.v1_4 | Should -Match '\.create-merge table ContractCommitments_final_v1_4'
}

It 'Defines BillingPeriods_transform_v1_4' {
$ingestionFiles.v1_4 | Should -Match 'BillingPeriods_transform_v1_4\(\)'
}

It 'Defines BillingPeriods_final_v1_4 table' {
$ingestionFiles.v1_4 | Should -Match '\.create-merge table BillingPeriods_final_v1_4'
}

It 'Defines InvoiceDetails_transform_v1_4' {
$ingestionFiles.v1_4 | Should -Match 'InvoiceDetails_transform_v1_4\(\)'
}

It 'Defines InvoiceDetails_final_v1_4 table' {
$ingestionFiles.v1_4 | Should -Match '\.create-merge table InvoiceDetails_final_v1_4'
}

It 'Costs_final_v1_4 block was extracted' {
$costsFinalV14Block | Should -Not -BeNullOrEmpty
}

It 'Costs_final_v1_4 does NOT include removed <_>' -ForEach @(
'ProviderName', 'PublisherName'
) {
$costsFinalV14Block | Should -Not -Match "(?m)^\s+$_\s*:"
}

It 'Costs_final_v1_4 includes new FOCUS 1.4 column <_>' -ForEach @(
'CommitmentProgramEligibilityDetails', 'InvoiceDetailId',
'ContractCommitmentBenefitCategory', 'ContractCommitmentCreated',
'ContractCommitmentDiscountPercentage', 'ContractCommitmentDurationType',
'ContractCommitmentFulfillmentInterval', 'ContractCommitmentLastUpdated',
'ContractCommitmentLifecycleStatus', 'ContractCommitmentModel',
'ContractCommitmentOfferCategory', 'ContractCommitmentPaymentInterval',
'ContractCommitmentPaymentModel', 'ContractCommitmentPaymentUpfrontPercentage'
) {
$costsFinalV14Block | Should -Match "(?m)^\s+$_\s*:"
}

It 'ContractCommitments_final_v1_4 includes FOCUS 1.4 column <_>' -ForEach @(
'BenefitCategory', 'ContractCommitmentApplicability', 'Created',
'DiscountPercentage', 'DurationType', 'FulfillmentInterval', 'LastUpdated',
'LifecycleStatus', 'Model', 'OfferCategory', 'PaymentInterval', 'PaymentModel',
'PaymentUpfrontPercentage', 'PricingCurrencyContractCommitmentCost'
) {
$contractCommitmentsFinalV14Block | Should -Match "(?m)^\s+$_\s*:"
}
}

Context 'HubSetup_v1_4.kql' {

It 'Defines <_>' -ForEach @(
'BillingPeriods_v1_4', 'CommitmentDiscountUsage_v1_4', 'ContractCommitments_v1_4',
'Costs_v1_4', 'InvoiceDetails_v1_4', 'Prices_v1_4', 'Recommendations_v1_4', 'Transactions_v1_4'
) {
$hubFiles.v1_4 | Should -Match "$_\(\)"
}

It 'Does NOT define singular ContractCommitment_v1_4' {
$hubFiles.v1_4 | Should -Not -Match 'ContractCommitment_v1_4\(\)'
}

It 'Costs_v1_4 unions all prior versions' -ForEach @(
'Costs_final_v1_0', 'Costs_final_v1_2', 'Costs_final_v1_4'
) {
$hubFiles.v1_4 | Should -Match "database\('Ingestion'\)\.$_"
}
}

Context 'HubSetup_Latest.kql aliases' {

It 'Aliases <_> to v1_4 (latest GA)' -ForEach @(
'BillingPeriods', 'CommitmentDiscountUsage', 'ContractCommitments',
'Costs', 'InvoiceDetails', 'Prices', 'Recommendations', 'Transactions'
) {
$hubFiles.Latest | Should -Match "(?ms)$_\(\)\s*\{\s*${_}_v1_4\(\)\s*\}"
}

It 'Does NOT alias to singular ContractCommitment' {
$hubFiles.Latest | Should -Not -Match '(?m)^ContractCommitment\(\)'
}

It 'Does NOT alias to v1_2 or older' {
$hubFiles.Latest | Should -Not -Match '_v1_2\(\)'
}
}

Context 'Bicep wiring' {

It 'app.bicep loads <_>' -ForEach @(
'IngestionSetup_v1_4.kql', 'HubSetup_v1_4.kql'
) {
$appBicep | Should -Match ([regex]::Escape($_))
}

It '.build.config lists <_>' -ForEach @(
'IngestionSetup_v1_4.kql', 'HubSetup_v1_4.kql'
) {
$buildConfig | Should -Match ([regex]::Escape($_))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ Create exports for each scope you want to monitor:
| Resource Group | `subscriptions/{subscription-id}/resourceGroups/{rg-name}` |

**Supported Datasets:**
- Cost and usage details (FOCUS) - `1.0`, `1.0r2`

- Cost and usage details (FOCUS) - `1.0`, `1.0r2`, `1.2`, `1.2-preview` (Cost Management export versions).
- Price sheet - `2023-05-01` (required for missing prices)
- Reservation details - `2023-03-01`
- Reservation recommendations - `2023-05-01` (required for Rate optimization report)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ All KQL queries are located in `references/queries/`:

**Database Rules:**
- Always use "Hub" database, NEVER "Ingestion"
- Function-based access: `Costs()`, `Prices()`, `Recommendations()`, `Transactions()`
- Function-based access: `Costs()`, `Prices()`, `CommitmentDiscountUsage()`, `ContractCommitments()`, `BillingPeriods()`, `InvoiceDetails()`, `Recommendations()`, `Transactions()`
- The unversioned functions (`Costs()`, etc.) return the latest GA FOCUS schema (v1_4). Use versioned functions (`Costs_v1_0()`, `Costs_v1_2()`, `Costs_v1_4()`) to pin to a specific schema.
- FOCUS 1.4 added: `AllocatedMethodId`, `AllocatedMethodDetails`, `AllocatedResourceId`, `AllocatedResourceName`, `AllocatedTags` (data-generator split cost allocation), `ContractApplied` (per-row contract commitment application), `ServiceProviderName` and `HostProviderName` (replacing removed `ProviderName` / `PublisherName`), `CommitmentProgramEligibilityDetails`, `InvoiceDetailId`, and 12 `ContractCommitment*` per-row columns. Also added three supplemental datasets: `ContractCommitments`, `BillingPeriods`, and `InvoiceDetails`.
- The `ContractCommitments()` function (FOCUS 1.4+) returns provider-confirmed contract commitment metadata — the dataset feeding `ContractApplied` JSON arrays on each cost row.
- The `BillingPeriods()` function (FOCUS 1.4+) returns billing period metadata (start/end/status) for use with `InvoiceDetailId` joins.
- The `InvoiceDetails()` function (FOCUS 1.4+) returns invoice line-item metadata associated with `InvoiceId` on the cost rows.

---

Expand Down
Loading