knowledgecenter-breadcrum

Knowledge Center

09 Sep, 2026

Embed a Power BI Report on a Dynamics 365 Form, Filtered to the Record You're Viewing

Posted on 09 Sep, 2026 by Vishal, Posted in Power BI Power Apps Dataverse Blog

Embed PowerBI Report To Form Blogs

Most Power BI reports in a Dynamics 365 environment live somewhere the user has to go and find: a dashboard, a workspace, a link in a Teams channel. That's fine for management reporting, but it's the wrong shape for someone working a single record. An account manager looking at an account doesn't want the tenant-wide contact list — they want this account's contacts, right there on the form.

The good news is you don't need one report per customer. You build one report and embed it on the form with a filter that resolves against the record currently open. Every account gets its own scoped view of the same report, and the platform does the substitution for you — no JavaScript.

This post walks through exactly that, using a real example: showing an account's child contacts on the Account form, filtered on parentcustomerid.

Step 1: Enable Embedded Content on the Environment

Nothing works until the environment allows Power BI embedding. In the Power Platform admin center, go to Environments → your environment → Settings, expand Product, and open Features.

Power Platform admin center showing Environments, DEV, Settings, with the Product section expanded and Features highlighted

Inside Features, find the Embedded content group and turn on the Power BI visualization embedding setting. Save.

Do this per environment. It is not a tenant-level switch, so an embed that works in DEV renders nothing in UAT or PROD until you flip it there too. Worth putting on your deployment checklist alongside the solution import — it's a configuration setting, so it won't travel in your solution.

Step 2: Collect the IDs From Your Report URL

Open the report in Power BI or Fabric and look at the address bar. Every value you need for the FormXML is in there.

Diagram mapping each segment of the Power BI report URL to its corresponding FormXML parameter

Using the example report URL:

https://app.fabric.microsoft.com/groups/me/reports/588b7578-927c-426e-a274-5600f6e54269/6d27f63725531a313acb?experience=fabric-developer
URL segment Goes into
groups/me PowerBIGroupId — see the note below
reports/588b7578-...-5600f6e54269 PowerBIReportId, and the reportId inside TileUrl
6d27f63725531a313acb The report page ID — optional, omit for the default page
?experience=fabric-developer Nothing. Ignore it.

The groups/me detail is worth pausing on. me means the report lives in My workspace, which has no workspace GUID. In that case PowerBIGroupId must be the all-zeros GUID:

00000000-0000-0000-0000-000000000000

If your URL shows a real GUID after /groups/ instead of me, use that GUID.

That said, a report in My workspace isn't a good place to leave this. My workspace belongs to you personally — colleagues can't be granted access to it, and it goes with your account if you move on. It's fine for proving the concept in DEV, which is exactly what the all-zeros GUID is doing here. Before this reaches real users, move the report into a shared workspace and swap in that workspace's GUID.

Step 3: Add a Tab and Section to the Form

In the modern form designer, open your table's main form (Account, here) and add a new tab with a section inside it. Give the tab a meaningful label — in this example, Contact (PowerBI Report).

The Account main form in the form designer with a new tab added, its Properties pane showing Label

Note the tab's Name, not just its label. Here it's tab_6, which makes the section inside it tab_6_section_1. You'll use those names in a moment to find the right place in the FormXML — on a form with a dozen tabs, that's the difference between a five-second search and a lot of scrolling.

Save and publish before moving on, so the tab and section actually exist in the form XML.

Placement is deliberate: putting the report on its own tab means it only loads when a user clicks that tab, keeping it off the critical path for everyone who opened the record for some other reason.

Step 4: Locate the Section in the FormXML Editor

The Power BI report control can't be added through the form designer — it has to be written into the form XML directly. Open XrmToolBox → FormXML Editor, load your table, and open the form.

XrmToolBox FormXML Editor with the Account form open, showing the tab_6 tab element and its tab_6_section_1 section highlighted

Search for the tab name from Step 3 (tab_6) and find its section. You want the rows element inside the section named tab_6_section_1 — that's where the control goes.

 

Step 5: Add the Control XML

Paste the following as a  inside that section's element.


  
    
      
    
      
        00000000-0000-0000-0000-000000000000
        588b7578-927c-426e-a274-5600f6e54269
        https://app.powerbi.com/reportEmbed?reportId=588b7578-927c-426e-a274-5600f6e54269
        { "Filter":"[{\"$schema\":\"basic\",\"target\":{\"table\":\"contact\",\"column\":\"parentcustomerid\"},\"operator\":\"In\",\"values\":[\"$a\"],\"filterType\":1}]", "Alias":{"$a":"accountid"}}
      
    
  

        

XrmToolBox FormXML Editor showing the added row with the filteredreport control and its PowerBI parameters highlighted

What you need to change

Value Change it? Notes
cell id Yes Must be a new, unique GUID. Generate a fresh one — don't reuse the one above. A duplicate cell ID fails validation or behaves oddly.
classid No {8C54228C-1B25-4909-A12A-F2B968BB0D62} is the Power BI report control. Same everywhere.
control id Optional Any name unique on the form. filteredreport is fine.
PowerBIGroupId Yes Your workspace GUID, or all-zeros for My workspace.
PowerBIReportId Yes Your report GUID from the URL.
TileUrl Yes Same report GUID again. Keep the app.powerbi.com/reportEmbed host.
PowerBIFilter Yes Your table, column, and alias attribute — see below.
rowspan Maybe 10 gives the report a reasonable height. Increase it if the report looks cramped.
label description Optional What shows above the control.

One easy-to-miss detail: TileUrl must be clean, unescaped URL text. If you copy this snippet out of a blog post or chat message, markdown link syntax sometimes comes along for the ride and you end up with stray [ and ](...) characters wrapped around the URL. That produces a control that silently fails to render. Paste into a plain text editor first and eyeball it.

Understanding the Filter — The Part That Makes It Contextual

The PowerBIFilter parameter is where the real work happens, and it's worth understanding rather than copying blind.

Diagram breaking down the PowerBIFilter JSON into target, operator, values, and the Alias substitution mechanism

Reformatted for readability, the filter is:

{
  "Filter": "[{
      \"$schema\":\"basic\",
      \"target\":{\"table\":\"contact\",\"column\":\"parentcustomerid\"},
      \"operator\":\"In\",
      \"values\":[\"$a\"],
      \"filterType\":1
  }]",
  "Alias": { "$a": "accountid" }
}

Breaking that down:

  • target — the table and column to filter on. These are names in the Power BI semantic model, not Dataverse logical names. If you renamed things during transformation, use the Power BI names. And the column has to actually be present in the model: if parentcustomerid was dropped to tidy up the dataset, there is nothing to filter on and no amount of XML will fix it.
  • operator: "In" — tests membership against the values array, so the same expression works whether you pass one value or several.
  • values: ["$a"]$a is a placeholder, not a value. This is the key idea.
  • Alias: {"$a": "accountid"} — tells the platform to replace $a at runtime with the accountid of the record currently open.

So when a user opens an account, the platform reads that record's accountid, substitutes it for $a, and the report renders showing only contacts whose parentcustomerid matches. Open a different account, get a different set. One report, filtered per record.

The alias value must be an attribute on the form's tableaccountid on Account here. A nice side effect of the platform doing the substitution: you never deal with getId() returning a braced, uppercase GUID, which is the classic failure mode when people build these filters in JavaScript instead.

Need to filter on more than one thing? Add a second clause to the filter array and a second alias ($b, and so on) to the same map.

Step 6: Validate, Save, and Publish

In the FormXML Editor, validate the XML, then Save and publish. Reload the form and click your new tab.

The Result

Here is the finished tab on an Account record. The report is embedded directly in the form, and every row it returns belongs to the account currently open:

The Account form with a Contact (PowerBI Report) tab showing a Power BI report filtered to contacts of the current account

Look at the accountidname column — every row reads the same account name. That is the filter doing its job. Open a different account and the same report returns a different set of rows, with no change to the report itself.

For contrast, this is the identical report opened directly in Power BI with no filter applied:

The same Power BI report opened in the Power BI service without a filter, showing contacts across all accounts

Same report, same semantic model. Here accountidname shows a mix of different accounts and blanks, because nothing is scoping it. The only difference between these two screenshots is the filter the form passes in — which is the whole idea: one report, filtered per record, instead of one report per customer.

If your report appears but shows everything, the filter isn't being applied — see troubleshooting below.

Security: Read This Before You Ship It

The two screenshots in the previous section make this concrete, so it's worth being blunt.

The record filter is a convenience feature, not a security boundary. The unfiltered screenshot is what the underlying report actually contains: every contact in the environment. The form filter narrows what gets displayed; it does not remove the rest of the data from the report.

What follows from that:

  • Dataverse security does not carry into Power BI. A user restricted to their own business unit in D365 has no such restriction inside the embedded report. Their access is governed by Power BI, not by their D365 security role.
  • Power BI workspace or report access is required separately. Users without it see an error or a blank control regardless of their Dynamics privileges.
  • If the data shouldn't be universally visible, configure row-level security (RLS) in Power BI based on the user's identity. That's the only mechanism that actually enforces restriction.

Use the record filter for convenience and RLS for security. Don't let the filtered-looking form lull you into thinking the data is scoped — it isn't.

Data Freshness

Users on a record form assume what they see is current. Depending on your model, it may not be:

  • Import mode with scheduled refresh means the report is only as fresh as the last refresh, potentially hours old.
  • DirectQuery over the Dataverse TDS endpoint gives near-real-time data, at some cost to report performance.

Pick deliberately and tell your users. On import mode, a visible last-refresh timestamp on the report page saves a surprising number of support tickets.

Troubleshooting

Symptom Likely cause
Control renders nothing at all Embedded content not enabled on this environment; or a malformed TileUrl (check for stray markdown characters)
Blank control or access error for some users Those users lack Power BI report/workspace access, or lack a Pro licence with a non-Premium workspace
Report loads but shows all records PowerBIFilter malformed, or the alias attribute isn't on the form's table
Visible filter error table or column doesn't match the Power BI model names, or the column isn't in the model
Works in DEV, not elsewhere Environment feature not enabled there; or report sits in My workspace, which nobody else can reach
Works for you, not colleagues You're testing as an admin — verify as an affected user
FormXML won't validate Duplicate cell id — regenerate a unique GUID
Data looks stale Refresh schedule on an import-mode semantic model

Limitations Worth Knowing

  • The control can't be configured from the form designer — FormXML is the only route, so this customization is effectively code and belongs in source control with the rest of your solution.
  • Embedded reports don't work offline in the mobile app, and mobile rendering has caveats that shift between releases. Test on the clients your users actually use.
  • The control adds load time. Its own tab mitigates this.
  • Report authoring stays in Power BI. Whoever maintains this afterwards needs Power BI access and skills, not just D365 ones.

Wrapping Up

The mechanism is small once it clicks: one report, one control in the FormXML, and an alias that resolves to the open record's ID. Most of the effort is in the surrounding details — enabling the feature per environment, confirming the filter column survived into the semantic model, generating a unique cell ID, and being honest about whether you need RLS.

Get those right and users get something they notice immediately: analytics where the work is happening, scoped to the thing they're already looking at.

Comment

This is a Required Field

Loading

Recent Updates

Blogs
08 Sep, 2026

How to Dynamically Retrieve SharePoint Drive IDs and List GUIDs for Power Automate

Introduction Many times, we come across scenarios where we need to populate a Word template in Dynamics 365 using Power…

READ MORE
Blogs
11 Aug, 2026

How to Remove an Unmanaged Layer from a Dataverse Email Template

Overview Email Templates in Microsoft Dataverse make it easy to reuse email content such as subject lines, message bodies, and…

READ MORE
How to Populate Dynamic Content Using Repeating Controls in a Word Document
Blogs
20 Jul, 2026

How to Populate Dynamic Content Using Repeating Controls in a Word Document

Introduction: Organizations frequently generate documents like invoices, certificates, quotations, transcripts, inspection reports etc. While single-value fields such as Name, Email,…

READ MORE