POWER BI QUESTIONS

How to Add an Export to Excel Button to a Power BI Report

An Export to Excel button needs an export implementation behind it. Ordinary Power BI report-button actions do not include Excel export. One supported developer route is a custom visual that creates a real XLSX from its bound rows, then requests a download through Power BI’s file download API.

Use the native Export data menu when it meets the job. The small order example below serves a specific click-to-download need. If the job is repeated file delivery, PBRS can schedule supported exports and destinations; a report button does not perform that recurring work.

  
THE DIRECT ANSWER

Choose a Supported Route Behind the Button

Three Excel download routes: visual export, custom visual download and a server-side PBRS portal action.

These are separate interaction routes, with separate access and testing needs.

Start with the interaction the reader needs. A one-off authorized extract may already be available through the visual’s More options > Export data menu. Adding a button is useful only when its data, format, destination and permission behavior are defined.

Route What the click does Boundary
Ordinary report button Runs navigation, a bookmark, Web URL or another configured action No documented native Excel-export action
Custom visual button Creates XLSX and calls the file download API Uses its own bound rows and download permission
Embedded application button Can call visual.exportData The response is CSV text, not XLSX

Ordinary button actions include bookmarks and Web URLs. A bookmark changes report state. A URL can open an approved application, but that application must implement the export.

The Power Automate visual documentation excludes export scenarios. The embedded exportData API is a separate CSV route. Renaming CSV text with an .xlsx extension does not create a workbook.

Choose the implementation

Native menu: Keep it when it supplies the required file.

Ordinary button: Its label does not implement Excel export.

Custom visual: Generate a real workbook before requesting download.

Embedded API: Handle its CSV response as CSV.

  
REQUIREMENTS AND LIMITATIONS

Define the Visual’s Data and Download Permissions

The example binds OrderId, Region and Amount, in that order. OrderId is a unique text key, Region is text and Amount is numeric. Configure aggregation to preserve the intended order-level detail. The button reads its own table data view, not a neighboring visual’s rows.

The synthetic source has O-001 in North for 1,200, O-002 in North for 800 and O-003 in South for 900. Selecting North should give two distinct keys totaling 2,000. Confirm that the slicer and report interactions affect this custom visual before comparing its output with another visual.

The file download API requires user consent and an administrator-controlled permission separate from ordinary export and sharing controls. Have the intended visual and data use approved; menu-export permission does not establish this permission.

Declare ExportContent in the visual’s capabilities. The extended download method needs API 5.3 or later and is supported in Service and Desktop, with a 30 MB file limit. This example uses a smaller 29,000,000-byte ceiling and rejects segmented or 1,000-row data views.

Report access and model security still determine the data supplied to the visual. Test the intended identity and workspace role. Check the actual generated file’s protection rather than assuming it inherits sensitivity labels or other report protections.

Define the button’s data and access

Data view: Bind the required fields to this visual.

Order detail: Keep unique text OrderId values.

Permission: Review the separate custom-visual download control.

Protection: Validate the generated file’s protection.

  
END-USER WORKFLOWS

Add and Test the Visual in the Intended Report

Ask the developer or report owner to provide the approved visual package described in the next chapter. Then test it in the report where readers will use it.

  1. Add the approved custom visual to a test report. Bind OrderId, Region and Amount in that order and configure the intended aggregation.
  2. Apply North to the custom visual. Inspect its displayed row count and reconcile the expected IDs.
  3. Using the intended export account, select Export current rows to Excel. Review the trust prompt and complete or cancel the download.
  4. Check the status and open the actual XLSX. For North, require O-001 and O-002, three columns and Amount totaling 2,000. O-003 must be absent.
  5. Repeat with South, restricted access and unavailable download permission. Retain the observed host results before distributing the implementation.

Cancellation must not display success. A completed download also does not prove the data is complete: compare keys, totals and the authorized click-time scope.

The example compiled and passed local checks with a mocked Power BI host. No live report, consent prompt or browser download was tested. The steps above supply that missing host evidence.

Test the report interaction

Binding: Use the agreed field order and aggregation.

Filters: Confirm slicers affect this visual.

Consent: Test completed and canceled downloads.

Host results: Record actual report behavior and file contents.

  
DEVELOPER AND API WORKFLOWS

Implement the Click, Workbook and Completion Checks

Create the custom-visual project using Microsoft’s development tutorial. Preserve its generated license comments, metadata and configuration. Set a compatible API version of at least 5.3 in pbiviz.json and install an approved XLSX serialization dependency. The local checks used Power BI visuals API 5.3.0, TypeScript 5.9.3 and SheetJS 0.20.3, with strict TypeScript checking and skipLibCheck for SDK declarations. Follow SheetJS’s official installation guidance and your dependency/version policy.

Add this configuration using the capabilities and data-view mapping guidance. The example requests up to 1,000 rows but refuses a view at that boundary to avoid accepting possible truncation.

{
  "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 the following Visual class in src/visual.ts, retaining the generated license notice. It copies the current rows before asynchronous permission checks, prevents overlapping clicks and serializes an actual XLSX. This focused example is not a complete deployment package: the project still needs its configuration, approved dependencies, field agreement and environment-specific tests.

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;
        }
    }
}

The local test decoded the workbook and recovered the two North rows with their text IDs. It covered denied permission, cancellation, segmented data, the 1,000-row boundary, duplicate keys, a numeric key, empty data and a host exception. A delayed permission response also tested an overlapping click and a filter change.

That overlapping request made one download call containing the original North rows. It verifies the click-time snapshot in the mocked test, but not real filter propagation, consent, tenant policy or downloads. Grouping, aggregation, filters and security can alter the data before the code receives it. Even a view below the ceiling needs comparison with an independent source baseline.

A different button in your application

If the user needs an approved reporting job rather than this visual’s immediate download, your portal can offer a “Prepare North sales workbook” action. Your backend checks the user’s permission, maps North to an isolated approved PBRS configuration, and calls the schedule. PBRS visual-data Excel settings define the selected visuals and workbook destination.

Use ExecuteScheduleAsync to start the job and return an application tracking reference. Retain PBRS’s ExecutionId, inspect its result, and confirm the workbook at the configured destination before showing it as ready. The PBRS execution guide gives the requests.

This is a separate server-side workflow. It does not add a native Excel-export action to an ordinary Power BI button, automatically inherit the viewer’s current custom-visual rows, or return XLSX bytes from the execution request. Authorise and map the report scope on the backend; keep users’ configurations separate so another request cannot change North’s output.

Choose the intended button behaviour

In-report download: Use the approved custom visual when the click must export its own bound rows; retain the click-time snapshot checks.

Portal action: Map an authorised application request to a tested PBRS workbook job and its configured destination.

Scope: Do not assume PBRS inherits the viewer’s current slicers or custom-visual data; apply the approved server-side selection.

Completion: Allow one intended job per request, inspect its result and validate the actual workbook before declaring it ready.

  
CHOOSE YOUR NEXT STEP

Accept the File Before Adopting the Button

Adopt the button only after the actual North output opens as XLSX with OrderId, Region and Amount, unique IDs O-001 and O-002, and a total of 2,000. Test it under the intended identity and real report interactions.

Keep the native menu when it already meets the task. Use the custom visual when this bounded, approved download behavior solves a real interaction need. Retain the package version, fields, identity, filters and observed host results. If the host blocks downloading, resolve the specific permission with the owner.

For scheduled file delivery, explore PBRS report automation. Return to Power BI Questions for the other Excel workflows.

If your readers need scheduled Excel files alongside the report's export button, share the guide with your colleagues to review deployment, licensing, security and your reporting requirements.

Download the PBRS Internal Evaluation Guide

Contributors:

Christian Ofori-Boateng

Christian Ofori-Boateng

CEO, ChristianSteven Software