Dummy BI
Back to Blog

TMDL Explained: What Power BI's Text Model Format Actually Contains

Dummy BI TeamApril 26, 20265 min read
TMDLPBIPPower BIDevOpsSemantic Model

If you've started working with PBIP (Power BI Project) format, you've seen the .tmdl files inside the definition/ folder of your semantic model. They're human-readable, they contain your entire model definition, and they're the reason PBIP makes version control possible.

But most people never actually open them. This post walks through what TMDL files contain, how they're structured, and why understanding them is useful even if you never edit them by hand.

What is TMDL?

TMDL stands for Tabular Model Definition Language. It's a text-based serialisation of the Analysis Services Tabular Object Model (TOM) — the same metadata structure that Power BI Desktop uses internally to represent your semantic model.

Before TMDL, the only text-based option was model.bim, a single JSON file containing the entire model. For large models, that file could be thousands of lines, making diffs noisy and merge conflicts painful.

TMDL splits the model into multiple files — one per table, one for relationships, one for the model root — using a concise, indentation-based syntax (similar to YAML but not quite).

File structure

Inside a PBIP project, the semantic model folder looks like this:

My Report.SemanticModel/
├── definition/
│   ├── model.tmdl
│   ├── relationships.tmdl
│   ├── expressions.tmdl
│   ├── tables/
│   │   ├── Sales.tmdl
│   │   ├── Date.tmdl
│   │   ├── Product.tmdl
│   │   └── _Measures.tmdl
│   └── roles/
│       └── RegionalManager.tmdl

Each file is self-contained. Change a measure in the Sales table, and only Sales.tmdl is modified. The diff shows exactly what changed.

What's inside a table file

A typical .tmdl table file contains everything about that table:

table Sales
	lineageTag: a1b2c3d4-e5f6-7890-abcd-ef1234567890

	partition 'Sales' = m
		mode: import
		source
			let
				Source = Sql.Database("server.database.windows.net", "SalesDB"),
				Sales = Source{[Schema="dbo", Item="FactSales"]}[Data]
			in
				Sales

	column SalesAmount
		dataType: decimal
		formatString: #,##0.00
		summarizeBy: sum
		sourceColumn: SalesAmount

	column OrderDate
		dataType: dateTime
		sourceColumn: OrderDate

	measure 'Total Sales' =
		SUM(Sales[SalesAmount])
		formatString: $#,##0.00

	measure 'Sales YTD' =
		TOTALYTD([Total Sales], 'Date'[Date])
		formatString: $#,##0.00

A few things to notice:

  • Indentation matters. Child properties are indented under their parent. This is how TMDL represents the hierarchy.
  • Partition definitions include the M (Power Query) expression. This is the actual data source — what query runs when you refresh.
  • Columns list their data type, source column, format, and summarisation behaviour.
  • Measures include the full DAX expression. Multi-line DAX is indented below the measure definition.

The relationships file

relationships.tmdl lists every relationship in the model:

relationship 5a6b7c8d-9e0f-1234-5678-9abcdef01234
	fromColumn: Sales.ProductKey
	toColumn: Product.ProductKey

relationship 6b7c8d9e-0f12-3456-7890-abcdef012345
	fromColumn: Sales.OrderDate
	toColumn: 'Date'.Date
	crossFilteringBehavior: bothDirections

Each relationship has a GUID, the two columns it connects, and optional properties like cross-filter direction and whether it's active. If a relationship is inactive, you'll see isActive: false.

The model file

model.tmdl is the root — it contains model-level properties like culture (locale), default power BI data source version, and any model-level annotations.

For most reports, it's short and rarely changes.

Why this matters for automation

Understanding TMDL structure is useful because it tells you what's possible to automate:

Datasource switching — To repoint a report at a different database, you need to rewrite the M expressions inside each table's partition definition, then update any table/column references in DAX measures, relationships, and visual definitions. Knowing that the M expression lives in a specific indented block within the table file makes this a parsing problem, not a guessing game.

Documentation — Everything you need for a model inventory is right there in the TMDL files. Tables, columns, measures, relationships, partition sources. You don't need a running instance of the model to extract this information.

Health checks — Detecting unused measures means comparing the set of measures defined in TMDL against the set referenced in visual definitions (PBIR JSON files) and in other measures' DAX expressions. Both sides of that comparison are text files you can parse.

DevOps diffing — Because each object is in a predictable location within its TMDL file, you can build meaningful diffs. "The Sales YTD measure changed its DAX expression between commits" is a much more useful statement than "model.bim changed."

Editing TMDL by hand

You can modify TMDL files directly in a text editor. Power BI Desktop will read them back when you open the PBIP project.

A few cautions:

  • Indentation must be tabs, not spaces. This is a common mistake if your editor auto-converts.
  • Lineage tags (GUIDs) must be present and unique. Don't remove them.
  • Measure expressions must be properly indented under the measure definition.

For quick fixes — renaming a measure, updating a format string — direct editing is fine. For structural changes like adding tables or relationships, it's safer to use Power BI Desktop or a tool that understands the full model schema.

The takeaway

TMDL is the serialisation layer that makes everything else possible — version control, automation, diffing, health checks. You don't need to master the format to benefit from tools that read it, but understanding the basics helps you make sense of what those tools are doing under the hood.

If you're working with PBIP projects, spend 10 minutes reading through one of your table files. You'll likely understand your own model better than you did before.


All posts

Published by Dummy BI

Related articles