How to Add an Export to Excel Button to a Power BI Report
Your regional manager has found the sales figures she needs in Power BI. Now she wants them in Excel, ready for her weekly planning.
PBRS can prepare and deliver that workbook when she requests it from your application, giving her a familiar way to get the file without asking you each time.
- The weekly sales review Start with the workbook your manager needs
- Your options Choose what should happen when she clicks
- Set it up Prepare North’s workbook before adding the button
- For developers Let the application request the workbook from PBRS
- Your next step Give her a workbook she can get on with
Start with the workbook your manager needs
Imagine your North region manager reviewing orders in Power BI on a Friday afternoon. She can see how the week went, but her next task is in Excel: sorting the orders and adding notes for Monday’s team meeting. She asks whether you can put an “Export to Excel” button next to the report.
It sounds like a small request. Before you build anything, sit down with her and try the visual’s More options > Export data menu. If the available export gives her the right workbook, showing her that option may be all the help she needs.
Suppose she wants the same North sales workbook each week, with a consistent worksheet and a familiar filename. You can set up that export and delivery in PBRS, then have a button in your reporting application request the saved job. She gets the workbook she recognises; you maintain its settings in one place.
There is an important distinction here: an ordinary Power BI report button does not have a built-in Excel-export action. The PBRS button belongs to your application or portal. If the button must sit inside the report itself and download its data directly, that calls for a different approach—a custom visual, which we cover below.
Ask to see the finished workbook
A file the manager already uses is a useful starting point. Look at the columns she sorts, the notes she adds and the figures she checks before deciding what the export should contain.
Choose what should happen when she clicks
For our manager’s regular workbook, a button labelled Prepare North sales workbook sets a clear expectation. The application asks PBRS to run the saved job, and PBRS prepares the file for its configured destination. The application can tell her that the workbook is being prepared and where to find it when it is ready.
That is different from downloading whatever rows happen to be visible at the moment. A PBRS job uses the report settings you have configured; it does not automatically pick up the manager’s current slicer selections. If she needs to choose a region or period in your application, your developer must connect that authorised choice to the correct job settings.
If her real need is an immediate download from inside the report, an approved custom visual may fit. It can offer a button that creates an XLSX workbook from the data supplied to that visual. It cannot simply reach across and copy the rows from the chart beside it.
Choose between those experiences with the manager before investing in the button. The illustration below shows how the native menu, a custom visual and a PBRS application request lead to different export processes.
What ordinary report buttons and embedded exports can do
A bookmark changes the report view, and a Web URL action opens a location. Neither creates an Excel workbook by itself. Microsoft’s report button guide describes the available actions.
The Power Automate visual documentation excludes export scenarios. If your application uses the embedded visual.exportData interface instead, its response is CSV text. A developer would need to create a real workbook; changing the file extension to .xlsx is not a conversion.
Tell her where the file will arrive
If the job emails the workbook, say so beside the button. If it saves the file for collection, explain where to find it. She should not have to guess whether clicking will start a download or send an email.
Prepare North’s workbook before adding the button
Start in PBRS with the sales report and a test destination. Select the relevant visuals, choose the Excel output, and configure the worksheet, filename and delivery location. Run the job once so you can review the actual file with the manager.
Use a small set of orders that is easy to recognise. In our illustrative example, North has order O-001 for 1,200 and O-002 for 800. South has O-003 for 900. North’s workbook should contain the first two orders, totalling 2,000, without the South order.
Open the workbook together and try what she does each Friday. Can she find the order numbers, sort the rows and work with the amounts as numbers? This is the point to adjust the export, while you are both looking at the result she needs.
Once that file is right, connect the application button to the tested job. Try the complete journey using her intended account: request North’s workbook, follow the progress message, then open the delivered copy. The application must check her access on the server and keep her job settings separate from another user’s request.
Try a request that does not finish normally
Agree what she should see if the workbook cannot be prepared. A clear message gives her a reason to contact the reporting team; a silent button leaves her clicking again. For the custom-visual alternative, also test a cancelled or denied download in the real report, because Power BI controls the download permission and consent.
Keep the first correct copy
Save the workbook you reviewed together. If the report changes later, it gives you and the manager a useful comparison when checking that the export still meets her needs.
Let the application request the workbook from PBRS
The manager should not need to understand an API to get her Friday file. Your application can offer the request and progress messages, while PBRS handles the workbook preparation and delivery behind them.
On the server, check that she is allowed to request North’s data and select the approved PBRS job. Keep credentials there as well. The button starts that job; it does not receive an Excel file directly from the execution request.
API example: request and monitor North’s saved workbook job
Begin with an existing single-report schedule that already produces the correct workbook in PBRS. The visual-data Excel guide explains the export settings. Use a valid access token and replace the example schedule ID 101 and connection values with your own.
POST {pbrsBaseUrl}/api/Schedule/ExecuteScheduleAsync
Authorization: bearer {pbrsAccessToken}
Content-Type: application/json
{"ScheduleType":"report","uniqueid":101,"RunBy":"North workbook requested"}
GET {pbrsBaseUrl}/api/Schedule/GetExecutionStatus?ExecutionId={ExecutionId}
Authorization: bearer {pbrsAccessToken}
Retain the returned ExecutionId and use it for status checks. Follow the execution guide to inspect the result and confirm delivery before telling the manager the workbook is ready.
If the portal offers a download link, your application must also provide authorised access to the completed file at its destination. PBRS’s execution response is a job reference, not XLSX bytes.
Keep each request tied to its approved regional configuration. Do not let a second user change shared settings while North’s job is running. If monitoring times out, check the existing execution before submitting another request so the manager does not receive duplicate files.
If you chose the direct-download experience inside Power BI, the separate example below shows that route. It builds a workbook from the custom visual’s own data and does not use the PBRS request above.
Alternative code example: download XLSX from a custom visual
For the custom visual, bind OrderId, Region and Amount in that order. Keep OrderId as a unique text key and Amount numeric, and configure aggregation to preserve the intended order-level detail. The button reads its own table data view, not a neighbouring visual’s rows.
Our illustrative data contains O-001 in North for 1,200, O-002 in North for 800 and O-003 in South for 900. Selecting North should supply two distinct IDs totalling 2,000. Confirm that the slicer and report interactions affect the custom visual itself.
The file download API has an administrator-controlled permission and user consent separate from ordinary export controls. Declare ExportContent in the visual’s capabilities and have the visual’s data use approved. The extended download method requires API 5.3 or later and supports Service and Desktop, with a 30 MB file limit.
The bounded example below uses a smaller 29,000,000-byte ceiling and refuses segmented or 1,000-row data views. Those checks reduce the chance of accepting an obviously limited view; they do not prove that every source record reached the visual. Model security, filters and aggregation still determine its input.
Inspect the generated workbook’s protection separately. Do not assume that a custom-created file automatically inherits every sensitivity label or protection attached to the report.
Build the custom visual
Create the project using Microsoft’s development tutorial, preserving its generated licence notices and configuration. Select a compatible API version of at least 5.3 in pbiviz.json and install an approved XLSX serialization dependency using SheetJS’s installation guidance and your version policy.
The following capabilities bind one table of export fields and request up to 1,000 rows. The implementation refuses a view at that boundary rather than treating it as proof of completeness. Review the capabilities and data-view mapping guidance when adapting it.
{
"privileges": [{"name": "ExportContent", "essential": true}],
"dataRoles": [{"name": "fields", "displayName": "Export fields", "kind": "GroupingOrMeasure"}],
"dataViewMappings": [{"table": {"rows": {"for": {"in": "fields"}, "dataReductionAlgorithm": {"top": {"count": 1000}}}}}],
"objects": {}
}
Place this Visual class in src/visual.ts, retaining the generated licence notice. It copies the current rows before awaiting permission, validates the agreed types and keys, creates a real workbook, and reports the host’s completion result.
import powerbi from "powerbi-visuals-api";
import * as XLSX from "xlsx";
import IVisual = powerbi.extensibility.visual.IVisual;
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
// Illustrative bounded export. This exports this visual's bound data view.
// It does not certify that the data view contains every source record.
export class Visual implements IVisual {
private service: powerbi.extensibility.IDownloadService;
private button: HTMLButtonElement;
private message: HTMLParagraphElement;
private view?: powerbi.DataView;
private busy = false;
constructor(options: VisualConstructorOptions) {
this.service = options.host.downloadService;
this.button = document.createElement("button");
this.message = document.createElement("p");
this.message.setAttribute("role", "status");
this.button.textContent = "Export current rows to Excel";
this.button.disabled = true;
this.button.addEventListener("click", () => { void this.download(); });
options.element.append(this.button, this.message);
}
public update(options: VisualUpdateOptions): void {
this.view = options.dataViews?.[0];
this.button.disabled = this.busy || !this.view?.table?.rows?.length;
if (!this.busy) {
this.message.textContent = `${this.view?.table?.rows?.length ?? 0} current visual rows. Validate the scope before exporting.`;
}
}
private async download(): Promise<void> {
if (this.busy) return;
this.busy = true;
this.button.disabled = true;
try {
// Freeze the click-time data before awaiting permission or consent.
const view = this.view;
const table = view?.table;
if (!table?.rows?.length) throw new Error("No current rows to export.");
if (view?.metadata.segment || table.rows!.length >= 1000) {
throw new Error("This bounded example refuses segmented or 1,000-row data views.");
}
const headers = table.columns.map(c => c.displayName);
if (headers.join("|") !== "OrderId|Region|Amount") {
throw new Error("Bind OrderId, Region and Amount in that order.");
}
const rows = table.rows!.map(row => [...row]);
const keys = new Set<string>();
for (const row of rows) {
if (row.length !== 3 || typeof row[0] !== "string" || !row[0] ||
typeof row[1] !== "string" || typeof row[2] !== "number" ||
!Number.isFinite(row[2]) || keys.has(row[0])) {
throw new Error("Check the required types and unique OrderId values.");
}
keys.add(row[0]);
}
const status = await this.service.exportStatus();
if (status !== powerbi.PrivilegeStatus.Allowed) {
throw new Error("Custom-visual download permission is unavailable.");
}
const workbook = XLSX.utils.book_new();
const sheet = XLSX.utils.aoa_to_sheet([headers, ...rows]);
XLSX.utils.book_append_sheet(workbook, sheet, "CurrentRows");
const content = XLSX.write(workbook, {bookType: "xlsx", type: "base64"});
// A conservative 29,000,000-byte ceiling below the host's 30 MB limit.
const padding = content.endsWith("==") ? 2 : content.endsWith("=") ? 1 : 0;
if (content.length * 3 / 4 - padding > 29000000) {
throw new Error("Workbook exceeds this example's size ceiling.");
}
const result = await this.service.exportVisualsContentExtended(
content, "Current-Visual-Rows.xlsx", "base64", "Current visual rows"
);
this.message.textContent = result.downloadCompleted
? `Download completed for ${rows.length} click-time rows. Validate the workbook before use.`
: "Download was not completed. No successful export is recorded.";
} catch (error) {
this.message.textContent = error instanceof Error ? error.message : "Export failed.";
} finally {
this.busy = false;
this.button.disabled = !this.view?.table?.rows?.length;
}
}
}
Test denied permission, cancellation, empty or segmented data, the row boundary, duplicate or non-text keys and host errors. Also test a delayed permission response while another click or filter change occurs. The retained click-time rows should be the ones written to the workbook. Compare them with an independent source baseline even when the view is below the example’s ceiling.
Make a missing file easy to trace
Keep the application’s request reference with the PBRS execution ID. When the manager asks about a workbook, your team can find the right run instead of asking her to click again.
Give her a workbook she can get on with
It is Friday afternoon again. Your manager requests the North workbook, opens the file and starts adding her notes for Monday. The button has done something useful: it has helped her move from reviewing the figures to planning with them.
Begin with the native export menu if it already meets that need. Where a repeatable workbook job makes more sense, test the file in PBRS before connecting the application. And if she later wants it waiting in her inbox every Friday, explore scheduled Excel delivery with PBRS.
Contributors:
Christian Ofori-Boateng
CEO, ChristianSteven Software
