Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 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: 10/28/2026
ms.date: 10/29/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
Expand All @@ -25,6 +25,15 @@ This article summarizes the features and enhancements in each release of the Fin

The following section lists features and enhancements that are currently in development.

### [Power BI reports](power-bi/reports.md)

_**Breaking change**_

- **Added**
- Added support for incremental refresh on the **Costs** table in storage reports. Power BI can now detect the **RangeStart**/**RangeEnd** filter on the `x_IncrementalRefreshDate` column. To learn more, see [Configure incremental refresh](power-bi/setup.md#configure-incremental-refresh) ([#2314](https://github.com/microsoft/finops-toolkit/issues/2314)).
- **Changed**
- Changed the `x_IncrementalRefreshDate` column in storage reports from text to Date/Time. Update any custom filters, measures, or visuals that treat it as text or a number ([#2314](https://github.com/microsoft/finops-toolkit/issues/2314)).

-->

<br><a name="latest"></a>
Expand Down
4 changes: 2 additions & 2 deletions docs-mslearn/toolkit/help/data-dictionary.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: FinOps toolkit data dictionary
description: This article describes the column names found in FinOps toolkit solutions, including columns from Cost Management and FOCUS.
author: flanakin
ms.author: micflan
ms.date: 06/21/2026
ms.date: 10/29/2026
ms.topic: concept-article
ms.service: finops
ms.subservice: finops-toolkit
Expand Down Expand Up @@ -113,7 +113,7 @@ Quick links:
| EffectiveCostRunningTotal | Derived. Used in Power BI to calculate the total `EffectiveCost` for the reporting period up to and including the specified day. For instance, the cost for the 15th will include the cost from the 1st through the 15th. |
| EffectivePricingCost | `EffectiveCost` in the pricing currency. |
| EffectiveUnitPrice | Amortized price per unit after commitment discounts. |
| <a name="i"></a>IncrementalRefreshDate | Derived. Numeric version of the `ChargePeriodStart` column to simplify setup for incremental refresh. |
| <a name="i"></a>IncrementalRefreshDate | Derived. Date/Time version of the charge date (for example, `ChargePeriodStart`) at midnight to simplify setup for incremental refresh. |
| InvoiceId | Unique identifier for the invoice the charge is included in. Only available for closed months after the invoice is published. Renamed from **x_InvoiceId** in FOCUS 1.2. |
| InvoiceIssuerId | Unique identifier of the organization that generated the invoice. |
| InvoiceIssuerName | Name of the organization that generated the invoice. Only supported for CSP accounts. Not supported for EA or MCA accounts that a partner manages due to data not being provided by Cost Management. |
Expand Down
28 changes: 25 additions & 3 deletions docs-mslearn/toolkit/power-bi/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Set up Power BI reports
description: Learn how to set up Power BI FinOps reports using the FinOps toolkit, customize visuals, and connect to your cost data for detailed analysis.
author: flanakin
ms.author: micflan
ms.date: 07/30/2026
ms.date: 10/29/2026
ms.topic: how-to
ms.service: finops
ms.subservice: finops-toolkit
Expand Down Expand Up @@ -105,8 +105,8 @@ The FinOps toolkit Power BI reports include preconfigured visuals, but aren't co
4. Append the container and export path, if applicable.
- **Number of Months** &ndash; Optional number of closed months you would like to report on if you want to always show a specific number of recent months. If not specified, the report will include all data in storage.
- **RangeStart** / **RangeEnd** &ndash; Optional date range you would like to limit to. If not specified, the report will include all data in storage.
- We generally recommend leaving these dates empty. They are included to support incremental refresh.
- If you need to configure incremental refresh, consider using [FinOps hubs](../hubs/finops-hubs-overview.md) with Data Explorer instead.
- We generally recommend leaving these dates empty. They are included to support incremental refresh. For steps, see [Configure incremental refresh](#configure-incremental-refresh).
- For larger datasets, consider using [FinOps hubs](../hubs/finops-hubs-overview.md) with Data Explorer instead.
- FinOps hubs with Data Explorer offers improved performance and is recommended for anyone monitoring over $100,000 in total spend.
- Storage reports only support ~$2 million of data without incremental refresh and ~$2 million per month in raw cost details. To learn more, see [Configure incremental refresh](/power-bi/connect-data/incremental-refresh-configure#define-policy).

Expand All @@ -121,6 +121,28 @@ If you run into any issues syncing your data, see [Troubleshooting Power BI repo

<br>

## Configure incremental refresh

Storage reports support incremental refresh on the **Costs** table. Incremental refresh is only needed when you have more cost data than Power BI can load in one refresh. If you're monitoring over $100,000 in total spend, consider [FinOps hubs](../hubs/finops-hubs-overview.md) with Data Explorer instead.

To configure incremental refresh:

1. Open the storage report in Power BI Desktop.
2. Select **Transform data** to open Power Query Editor.
3. Set **RangeStart** and **RangeEnd** to a small, recent date range, like the first day of last month and the first day of this month.
- These values only apply in Power BI Desktop. The incremental refresh policy replaces them after you publish.
4. Select **Close & Apply**.
5. In the **Data** pane, right-click the **Costs** table and select **Incremental refresh**.
6. Turn on **Incrementally refresh this table**, set the archive and refresh periods, and then select **Apply**.
- The **Costs** query filters the `x_IncrementalRefreshDate` column by **RangeStart** and **RangeEnd**.
7. Publish the report and refresh the semantic model in the Power BI service.

Rows without a charge date (`x_IncrementalRefreshDate` is empty) are kept in every partition when a range is set, so they can be counted more than once. These rows are rare, but if you see inflated totals after enabling incremental refresh, check for them.

If you leave **RangeStart** and **RangeEnd** empty, the **Costs** query doesn't filter by them and the report uses **Number of Months** as before. To learn more, see [Configure incremental refresh](/power-bi/connect-data/incremental-refresh-configure).

<br>

## Use a SAS token to connect data to a report

Shared Access Signature (SAS) tokens allow you to connect to a storage account without end user credentials or setting up a service principal. To connect Power BI reports to your data via SAS tokens:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ expression ftk_Storage = ```
let
segments = if d = null then null else Text.SplitAny(Text.From(d), "/- T"),
output =
try Number.From(Date.From(
try DateTime.From(Date.From(
if Text.Length(segments{0}) = 4 then segments{0} & "-" & Text.PadStart(segments{1}, 2, "0") & "-" & Text.PadStart(segments{2}, 2, "0")
else if Text.Length(segments{2}) = 4 then segments{2} & "-" & Text.PadStart(segments{0}, 2, "0") & "-" & Text.PadStart(segments{1}, 2, "0")
else d,
Expand All @@ -314,11 +314,11 @@ expression ftk_Storage = ```
else if data = "reservationdetails" then [UsageDate]
else if data = "reservationtransactions" then [EventDate]
else null // Will not support incremental refresh
)
), type datetime
Comment thread
flanakin marked this conversation as resolved.
),
FilterRowsByDate = if (start = null and end = null) then RefreshDate else Table.SelectRows(RefreshDate, each
(start = null or [x_IncrementalRefreshDate] = null or [x_IncrementalRefreshDate] >= Number.From(DateTime.From(start)))
and (end = null or [x_IncrementalRefreshDate] = null or [x_IncrementalRefreshDate] < Number.From(DateTime.From(end)))
(start = null or [x_IncrementalRefreshDate] = null or [x_IncrementalRefreshDate] >= Date.StartOfDay(DateTime.From(start)))
and (end = null or [x_IncrementalRefreshDate] = null or [x_IncrementalRefreshDate] < Date.StartOfDay(DateTime.From(end)))
),

// TODO: Try using "_" instead of "FilterRowsByDate"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,8 @@ table Costs
annotation PBI_FormatHint = {"isGeneralNumber":true}

column x_IncrementalRefreshDate
dataType: string
dataType: dateTime
Comment thread
flanakin marked this conversation as resolved.
formatString: General Date
lineageTag: 73487d4f-b0dd-4d41-8d9e-739636dabab8
summarizeBy: none
sourceColumn: x_IncrementalRefreshDate
Expand Down Expand Up @@ -1733,7 +1734,14 @@ table Costs
*/

// Sort columns alphabetically
Output = Table.ReorderColumns(AHB, List.Sort(Table.ColumnNames(AHB)))
Sorted = Table.ReorderColumns(AHB, List.Sort(Table.ColumnNames(AHB))),

// Incremental refresh -- Must be the last step so Power BI detects the RangeStart/RangeEnd filter
// Matches the ftk_Storage row filter: whole days, >= start and < end, keeps rows without a date, and keeps all rows when RangeStart/RangeEnd are empty
Output = Table.SelectRows(Sorted, each
(RangeStart = null or [x_IncrementalRefreshDate] = null or [x_IncrementalRefreshDate] >= Date.StartOfDay(DateTime.From(RangeStart)))
Comment thread
flanakin marked this conversation as resolved.
and (RangeEnd = null or [x_IncrementalRefreshDate] = null or [x_IncrementalRefreshDate] < Date.StartOfDay(DateTime.From(RangeEnd)))
)
in
Output
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ table Prices
annotation SummarizationSetBy = Automatic

column x_IncrementalRefreshDate
dataType: string
dataType: dateTime
formatString: General Date
lineageTag: 55e08448-a8ac-4f16-98f3-915f443d4117
summarizeBy: none
sourceColumn: x_IncrementalRefreshDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ table ReservationDetails
annotation PBI_FormatHint = {"isGeneralNumber":true}

column x_IncrementalRefreshDate
dataType: string
dataType: dateTime
formatString: General Date
lineageTag: 7631c2d6-b330-42de-8588-967752ced645
summarizeBy: none
sourceColumn: x_IncrementalRefreshDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ table ReservationRecommendations
lineageTag: a9b8f96b-84d0-4176-95a1-65eade30a2ef

column x_IncrementalRefreshDate
dataType: string
dataType: dateTime
formatString: General Date
lineageTag: 0d03437c-a892-4d20-9e24-e1ae25b31e47
summarizeBy: none
sourceColumn: x_IncrementalRefreshDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ table ReservationTransactions
annotation SummarizationSetBy = Automatic

column x_IncrementalRefreshDate
dataType: string
dataType: dateTime
formatString: General Date
lineageTag: db9b73cb-8a9d-455c-88dd-41a7fd42bb2e
summarizeBy: none
sourceColumn: x_IncrementalRefreshDate
Expand Down
23 changes: 23 additions & 0 deletions src/powershell/Tests/Lint/PowerBIReports.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,27 @@ Describe 'Demo data source' {
$inList = $text -match ('"' + [regex]::Escape($account) + '"')
$inList | Should -BeTrue -Because "$account should be in the ftk_DemoFilter storage account list"
}
}

Describe 'Storage incremental refresh' {
BeforeAll {
$tablesDir = Join-Path $PSScriptRoot '../../../power-bi/storage/Shared.Dataset/definition/tables'
}

It 'Ends the Costs query with a RangeStart/RangeEnd row filter' {
# Power BI only offers incremental refresh when the table's own query filters rows with RangeStart and RangeEnd
$text = Get-Content (Join-Path $tablesDir 'Costs.tmdl') -Raw
$isLastStep = $text -match '(?s)Output\s*=\s*Table\.SelectRows\((?<filter>(?:(?!Table\.SelectRows).)*?)\)\s*in\s+Output\s*\r?\n\s*```'
$isLastStep | Should -BeTrue -Because 'the final step of the Costs query must be Table.SelectRows so Power BI detects the filter'
$filter = $Matches['filter']
$filter | Should -Match '\bRangeStart\b'
$filter | Should -Match '\bRangeEnd\b'
$filter | Should -Match 'x_IncrementalRefreshDate'
}

It 'Types x_IncrementalRefreshDate as dateTime in <_>' -ForEach @('Costs', 'Prices', 'ReservationDetails', 'ReservationRecommendations', 'ReservationTransactions') {
$text = Get-Content (Join-Path $tablesDir "$_.tmdl") -Raw
$isDateTime = $text -match '(?m)^\tcolumn x_IncrementalRefreshDate\r?\n\t\tdataType: dateTime\b'
$isDateTime | Should -BeTrue -Because 'incremental refresh needs a Date/Time column to compare with RangeStart and RangeEnd'
}
}
Loading