Quick summary
Summarize this blog with AI
Choose the export by what the recipient needs: a fixed data snapshot, the matrix layout, a refreshable workbook, or a recurring formatted attachment. Then check the file's exact rows and values against the intended filters. A workbook that opens successfully can still contain the wrong population, omit a category, or misrepresent a total.
This guide uses one small Power BI report to produce a filtered Excel snapshot and check its data. The instructions refer to Power BI Desktop and the Power BI service as documented in October 2026; available options depend on permissions, report settings, and your environment. The worked input is entirely fictional.
Choose the output before choosing the button
Ask the recipient what they will do with the file. Someone who needs to reconcile category totals needs a different artifact from someone who needs a printable matrix or a workbook they can refresh next week.
| Required result | Route to evaluate | Acceptance check |
|---|---|---|
| A fixed table for reconciliation | Export summarized data from the visual to a static Excel workbook. | Expected category keys, measures, filters, and source-as-of time. |
| A matrix resembling the report | Data with current layout, where supported. | Headers, row groups, totals, and readable cells as well as values. |
| A workbook that reconnects to the model | A live-connected export or Analyze in Excel. | Successful refresh under the intended recipient's access. |
| A recurring formatted Excel attachment | A paginated report with an appropriate subscription or export process. | Parameters, delivery, workbook layout, and source freshness. |
Summarized export keeps aggregation. Current layout preserves presentation; it does not turn summaries into transactions. Underlying export is model-dependent. Desktop offers summarized CSV export; the service also offers Excel routes. See Microsoft's visual export documentation.
Analyze in Excel and connected exports require suitable permissions, tenant settings, and supported Excel access. Refreshing a workbook, refreshing its semantic model, and loading the original source are separate operations. A connection is useful when refresh is the requirement; it does not preserve an immutable historical snapshot.
For repeated attachments, distinguish report types: the ExportToFile API supports XLSX for paginated reports, not ordinary Power BI reports. A flow using that API does not automatically become a visual-to-Excel exporter.
Build a five-row report with a known answer
In Power BI Desktop, use Enter data or import this CSV and name the table Sales. Set SaleID to Whole number; CustomerID, Region, and Category to Text; and Amount to Fixed decimal number. This single-table example requires no relationships.
SaleID,CustomerID,Region,Category,Amount
101,A,West,Hardware,120
102,B,West,Hardware,80
103,A,West,Services,50
104,C,East,Hardware,300
105,D,West,Services,0
Create these two measures separately:
Revenue = SUM ( Sales[Amount] )
Customers = DISTINCTCOUNT ( Sales[CustomerID] )
Add a table visual with Category, Revenue, and Customers. Apply a visual filter for Region = West. Keep all categories selected and avoid Top N filters, hierarchies, or other selections in this fixture. Use the exact displayed column names above.
| Category | Revenue | Customers |
|---|---|---|
| Hardware | 200 | 2 |
| Services | 50 | 2 |
The selected sales are 101, 102, 103, and 105. Sale 104 is excluded. The zero-amount sale still contributes customer D to Services. Overall revenue is 250, and the overall distinct-customer count is 3: A, B, and D. Adding the category customer counts gives four memberships because A appears twice. If this distinction is unfamiliar, see the worked DAX totals guide.
Export and retain the original snapshot
- If authorized, publish this fictional report to a test workspace and open it in the Power BI service.
- Confirm the table still has the West filter and the expected two category rows. Record the report, selection, export time, and source-as-of information separately.
- Open the visual's export menu, choose summarized data and the static Excel workbook option, and save the result. If the option is unavailable, check the report owner's export settings and your permissions.
- Open the workbook and inspect the category rows, values, filter information, and any truncation notice. Retain this original file unchanged.
- For the checker below, create a separate review sheet containing only the three exact headers and the two category rows. Exclude filter-description rows, blank framing rows, and grand totals explicitly. Save that sheet as UTF-8, comma-delimited CSV named
export.csv. Use plain decimal formatting without currency symbols or thousands separators, preserving the values. The checker expects a period decimal mark and integer customer counts; verify the saved text if your locale uses other separators.
The review copy has this data contract; row order is immaterial:
Category,Revenue,Customers
Hardware,200,2
Services,50,2
Retain the original workbook and its filter context beside this copy. Converting selected cells to CSV is a controlled step for this tiny exercise, not an automated parser for arbitrary workbook layouts. Do not silently delete an unexpected category or change a value to make the check pass.
Check exact rows, not only the grand total
The checker below verifies this fixture's three-column contract, unique category rows, and exact values. It was validated in Python 3.9.6 against 45 passing and failing cases. It should reject Hardware 180 and Services 70 even though those wrong amounts still total 250.
Save the following complete program as check_export.py. It uses only the Python standard library and calculates the expected rows independently from the fictional sales input.
import csv
import re
import sys
from collections import defaultdict
from decimal import Decimal
# Fictional source input; never a live Power BI query.
SALES = [
(101, 'A', 'West', 'Hardware', '120'),
(102, 'B', 'West', 'Hardware', '80'),
(103, 'A', 'West', 'Services', '50'),
(104, 'C', 'East', 'Hardware', '300'),
(105, 'D', 'West', 'Services', '0'),
]
HEADERS = ['Category', 'Revenue', 'Customers']
def expected_rows():
amounts = defaultdict(Decimal)
customers = defaultdict(set)
for _, customer, region, category, amount in SALES:
if region == 'West':
amounts[category] += Decimal(amount)
customers[category].add(customer)
return {k: (amounts[k], len(customers[k])) for k in amounts}
def check_export(path):
actual = {}
with open(path, encoding='utf-8-sig', newline='') as handle:
reader = csv.DictReader(handle, strict=True)
if reader.fieldnames != HEADERS:
raise ValueError('Expected exactly Category,Revenue,Customers headers')
for line, row in enumerate(reader, start=2):
if None in row or any(row[k] is None for k in HEADERS):
raise ValueError(f'CSV record {line}: missing or extra cells')
key = row['Category']
if not key or key != key.strip():
raise ValueError(f'CSV record {line}: blank or padded category')
if key in actual:
raise ValueError(f'CSV record {line}: duplicate category {key!r}')
if not re.fullmatch(r'-?(0|[1-9][0-9]*)(\.[0-9]+)?', row['Revenue']):
raise ValueError(f'CSV record {line}: revenue must be an unformatted decimal')
if not re.fullmatch(r'0|[1-9][0-9]*', row['Customers']):
raise ValueError(f'CSV record {line}: customers must be a nonnegative integer')
actual[key] = (Decimal(row['Revenue']), int(row['Customers']))
expected = expected_rows()
missing = sorted(expected.keys() - actual.keys())
extra = sorted(actual.keys() - expected.keys())
if missing or extra:
raise ValueError(f'Wrong categories: missing={missing}, extra={extra}')
for key in sorted(expected):
if actual[key] != expected[key]:
raise ValueError(f'{key}: got {actual[key]}, expected {expected[key]}')
return len(actual)
if __name__ == '__main__':
if len(sys.argv) != 2:
raise SystemExit('Usage: python check_export.py exported.csv')
try:
count = check_export(sys.argv[1])
except (OSError, UnicodeError, csv.Error, ValueError) as exc:
raise SystemExit(f'FAIL: {exc}')
print(f'PASS: {count} category rows match the West fixture exactly.')
With export.csv in the same folder, run:
python check_export.py export.csv
The unchanged two-row fixture prints:
PASS: 2 category rows match the West fixture exactly.
A rejected file exits with a nonzero status and an explanatory FAIL: message. The program checks this small teaching contract; use an independently agreed source reference when adapting it to a real report.
Validation scope: this check establishes whether a CSV matches the fictional expected result. It does not establish how Power BI generated the workbook, whether the source was complete, or whether a recipient has appropriate access. Power BI Desktop/Service, native DAX, Excel export or refresh, paginated rendering, and RLS execution were not run for this walkthrough.
Make the failure cases deliberate
| Change in a separate test copy | Expected result |
|---|---|
| Remove Services or repeat Hardware. | Fail: missing or duplicate category. |
| Change Hardware to 180 and Services to 70. | Fail despite the unchanged revenue total. |
| Include East's 300 in Hardware. | Fail: the export no longer matches West. |
| Drop the zero-amount sale before calculating customers. | Services incorrectly becomes one customer; fail. |
| Leave Revenue blank or rename its header. | Fail visibly; do not reinterpret missing data as zero. |
| Produce no category rows. | Fail for this fixture, whose expected population is nonempty. |
For a real report, agree the expected population independently. An empty result can be valid after a known filter, but a missing source batch can produce the same empty file. Record which case applies before distributing it. Use the metric reconciliation workflow when the report and the source disagree.
Troubleshoot the output you actually received
The matrix looks flattened. Check the selected export route before redesigning measures. If layout matters, inspect a supported current-layout export. For a paginated workbook, Microsoft's Excel renderer guidance explains how report-item boundaries and alignment can create extra columns or merged cells. Inspect the rendered file; an alignment change is not a universal repair.
The workbook is missing rows. Check filters, access, aggregation, and export limits. Published ceilings are not completeness guarantees: static XLSX can contain fewer than its 150,000-row maximum, and current-layout matrices use a 150,000-data-intersection limit. Inspect warnings and compare expected keys, not file size.
The owner gets different data from the recipient. Test with the intended consumer's approved access. Do not grant a broader workspace role just to obtain a matching export. The dynamic RLS guide explains why author access is an unsuitable consumer test. Also consider where a saved snapshot can subsequently be shared.
A scheduled file is stale. Delivery and refresh are separate. Microsoft's subscription documentation states that a Run now test does not refresh the underlying semantic model. Establish source readiness before delivery using the recurring-report release workflow.
FAQ
Can one visual export combine two filtered report tables?
Do not assume it does. Define each output's grain and filters, then choose a deliberately designed combined table, paginated report, or separate extraction process. This walkthrough exports one table and makes no claim to automate multi-visual delivery.
Why not use underlying data for every request?
Choose the grain that answers the request. This fixture needs two category rows; a list of sales would answer a different question. More detail is not automatically the right result.
Does passing the checker mean the report is ready to share?
It means this CSV matches this fixture. A production release also needs the agreed source population, freshness, recipient access, and a usable workbook. Keep those checks distinct so one successful test does not stand in for the others.