THE DIFF
Rename a column without updating relationships.tmdl, and the model loads without complaint. No warning. The column reference in the relationship is now stale — Desktop silently drops the relationship on load, or throws an error on refresh depending on the engine version. Either way, the filter stops working and nothing in the file told you it was about to.
This is TMDL's double edge. The format is readable in a way a .pbix file never was. But readable means editable, and editable means breakable in ways that don't surface until runtime.
relationships.tmdl is one of two files in the PBIP definition folder that carry the model's structural integrity. The other is the roles/ directory. Neither is complicated to read. Both reward careful attention in a diff.
What relationships.tmdl looks like
The file lives at definition/relationships.tmdl. It is flat — every relationship in the model, in a single file, in declaration order. A typical entry:
relationship 'a3f2e1d4-5b6c-7d8e-9f0a-1b2c3d4e5f6a'
fromTable: Sales
fromColumn: OrderDateKey
toTable: 'Date'
toColumn: DateKey
The identifier is a GUID — generated by Desktop, not meaningful to you. The meaning lives in fromTable, fromColumn, toTable, and toColumn. Those four fields are the relationship.¹
Two properties worth knowing.
Active vs inactive. A relationship with no isActive property is active — that's the default. An inactive relationship adds one line:
relationship 'b4c5d6e7-f8a9-0b1c-2d3e-4f5a6b7c8d9e'
fromTable: Sales
fromColumn: ShipDateKey
toTable: 'Date'
toColumn: DateKey
isActive: false
One line differentiates an active relationship from an inactive one. In a diff, that line is easy to miss.
Cross-filter direction. The default is single-direction — filter propagates from the lookup side to the fact side. When bidirectional is set, the file says:
crossFilteringBehavior: bothDirections
That property is only written when it differs from the default. If you don't see crossFilteringBehavior, the relationship is single-direction. If you do see it, read why it's there before approving the PR.
What the roles/ folder looks like
Roles live in definition/roles/. One .tmdl file per role:
role 'Region Manager'
modelPermission: read
tablePermission Sales
filterExpression: 'Sales'[Region] = USERPRINCIPALNAME()
Three things to know.
First, filterExpression is DAX. It is not validated at save time. If the column name is wrong or the expression is malformed, you find out when you test the role — or when a user reports that their data looks wrong.
Second, modelPermission: read is standard for RLS roles. write exists but is uncommon — it allows users in the role to edit the model in the Service, which is rarely the intent.
Third, each role is self-contained. Power BI has no role inheritance. If a user is in two roles, both filterExpression values are evaluated and OR'd together. This is not obvious, and it's the source of most "my RLS is showing too much data" reports.
Where things break
Column renames. Rename OrderDateKey to OrderDate in the table file. The relationship in relationships.tmdl still says fromColumn: OrderDateKey. TMDL does not cascade renames. You update the table file and forget the relationship file. The model fails on load. The fix is straightforward — search relationships.tmdl for the old column name after any rename — but you have to remember to do it.
Bidirectional added as a quick fix. Someone's report isn't filtering correctly. They add crossFilteringBehavior: bothDirections to the relationship. The immediate problem goes away. Weeks later, a different measure returns wrong numbers — DAX found an ambiguous filter path and resolved it in a way that's internally consistent but factually wrong. The cause is buried in the commit history.²
Role filter errors at runtime. A column gets renamed after the role was written. The filterExpression still references the old name. The role appears valid in the file — the problem is semantic, not syntactic. Users in the role see no data or an error. This one is particularly easy to miss in review.
OR logic in overlapping roles. A user is in both Region Manager and Finance. Region Manager filters to rows where [Region] = USERPRINCIPALNAME(). Finance filters to rows where [Department] = "Finance". The user sees all Finance rows regardless of region — because satisfying either filter is sufficient. This is correct Power BI behavior. It surprises teams that designed their roles expecting AND logic.
What to watch in a diff
When reviewing a PR that touches either file, these are the lines that matter:
Any change to
fromColumnortoColumn— verify the column still exists under that name.isActive: falseappearing or disappearing — this changes what CALCULATE sees without touching any DAX.crossFilteringBehavior: bothDirectionsadded anywhere — ask why before approving.Any change to
filterExpression— test the role with a real user or a role-switching tool in Desktop.
The TMDL format gives you the visibility to catch these in review. That's the point. But visibility only helps if you're looking at the right lines.
Next issue: Microsoft called three different things 'AI for Power BI' at Build. They're not the same thing.
THE DELTA
Visual calculations are now GA
Visual calculations — running sums, moving averages, percent of parent — moved to general availability in the May 2026 Desktop release. They operate on aggregated data inside the visual, not on the semantic model, which means they don't appear in DAX measures, don't show up in TMDL, and don't travel with the model when it's consumed downstream. If your report needs a calculation that only makes sense inside a specific visual context, this is the right tool. If the calculation needs to be reusable across visuals or accessible from other reports, it still belongs in the model as a measure.³
Web modeling is now the default for semantic model authors
Users with edit permissions on a semantic model are now taken directly to the web modeling experience instead of the model details page. Most actions from the model details page are integrated into model view. Fewer clicks to get to the relationships pane and measure editor. Small change, but if you spend time in the Service editing models without Desktop open, the friction just dropped.³
New Get Data experience in Desktop (Preview)
A redesigned Power Query Get Data dialog ships in preview — unified data source discovery, streamlined connection flow, keyboard navigation, dark mode. It's aligned with the Fabric and Excel versions of the same experience. Worth enabling in Options to evaluate before it becomes the default. No functional changes to Power Query itself; this is the front door, not the engine.⁴
THE ARCHITECTURE NOTE
Bidirectional relationships: the one-line change with a long tail

Single-direction relationships pass filters one way — from the lookup table to the fact table. That's the star schema default, and it's correct for most models.
Bidirectional relationships pass filters both ways. Adding crossFilteringBehavior: bothDirections in TMDL is one property in one file. The impact can reach every measure that touches a table on either side of that relationship, because DAX now has to resolve which path to use when multiple routes exist — and when it can't determine the right one, it picks one. Silently.
The legitimate use case is a many-to-many bridge — a relationship designed explicitly to pass filters in both directions because the bridge table is the analysis object. Product-category bridges where products belong to multiple hierarchies. Attendance rosters. Shopping carts. The shape of the problem tells you bidirectional is right.
The wrong use case is "my slicer isn't filtering this visual." That's a symptom. Adding bothDirections is often the fastest path to making the symptom disappear. The symptom usually means the model design needs adjustment, not that the relationship needs to go both ways.
In a code review: crossFilteringBehavior: bothDirections appearing in a diff should prompt a question, not a block. "Is this a bridge table pattern, or is this fixing a filter symptom?" The answer tells you whether to approve or ask for a redesign.
WORTH READING
TMDL reference — The full property list for relationships and roles. Two pages. Worth reading once so you know what the format can express. (Microsoft Learn)
Bidirectional relationships and ambiguity in DAX — Alberto Ferrari's piece on why bidirectional filtering creates ambiguous paths and what the engine does about it. Required reading before adding bothDirections to anything. (SQLBI)
PBIP known limitations — The list changes as features move from preview to GA. Worth checking before committing to PBIP for a production model. (Microsoft Learn)
The TMDL folder is most useful when you read it before something breaks — not after.
Did you learn something?
Sources
TMDL reference — relationships and roles — Microsoft Learn
Bidirectional relationships and ambiguity in DAX — SQLBI, Alberto Ferrari
Power BI May 2026 update — New Get Data experience (Preview) — Microsoft Learn

