TMDL Authoring Rules
Syntax Rules (MUST follow)
- TMDL uses tab indentation — every nesting level is exactly one tab (
\t), NOT spaces. Spaces cause validation errors.
- PowerShell: use
`t
- Bash: use
$'\t' or literal tabs
- Objects declared by type + name:
table Customer, column ProductId, measure 'Total Sales'
- Names with spaces or special chars (
., =, :, ') must be in single quotes: column 'Order Date'
- Descriptions use
/// placed ABOVE the object — do NOT use the description property
// comments are NOT supported in TMDL
- Do NOT add
lineageTag on new objects — it is auto-generated
- Multi-line DAX must be enclosed in triple backticks (
```)
- Place measures before columns in table definitions
formatString is required on every measure
- Never set
dataType on measures — it is inferred from DAX
Naming Conventions
- Tables: business-friendly, no
Fact/Dim prefixes. Plural for facts (Sales), singular for dimensions (Product)
- Columns: readable with spaces (
Order Date, Unit Price)
- Measures: clear patterns (
Total Sales, # Customers). Time intelligence: [measure], [measure (ly)], [measure (ytd)])
Column Rules
| Property |
Rule |
dataType |
Required. Use int64, decimal, string, dateTime, boolean. Avoid double |
sourceColumn |
Must match partition source column name exactly |
isHidden |
Set for ID columns, foreign keys, system columns |
summarizeBy |
none for non-aggregatable numerics (IDs, postal codes, year numbers) |
isAvailableInMdx |
false for hidden columns not used in sort-by or hierarchies |
sortByColumn |
For text needing non-alphabetical sort (month names → month number) |
Measure & DAX Rules
- Always set
formatString — Currency: $#,##0.00 | Percentage: 0.00% | Integer: #,##0 | Decimal: #,##0.00
- Use
DIVIDE() instead of / for safe division
- Never use
IFERROR — causes performance degradation
- Prefix
VAR names with _: VAR _totalSales = ...
- Use
displayFolder to organize measures into logical groups
- Add
/// descriptions to explain business logic
Relationship Rules
fromColumn: = many-side (fact); toColumn: = one-side (dimension)
- Create relationships BEFORE measures that depend on them
- Default:
crossFilteringBehavior: oneDirection; add bothDirections only when needed
isActive: false for role-playing dimensions; use USERELATIONSHIP() in DAX
- Both sides must have matching
dataType
- Set
isKey: true on dimension primary key columns
- Hide foreign keys on fact tables (
isHidden: true)
- No composite keys — use a single surrogate integer key
Calculation Groups
table 'Time Intelligence'
calculationGroup
calculationItem Current = SELECTEDMEASURE()
calculationItem YTD = CALCULATE(SELECTEDMEASURE(), DATESYTD('Date'[Date]))
column 'Time Intelligence'
dataType: string
partition 'Partition_Time Intelligence' = calculationGroup
calculationGroup keyword has NO name — just the keyword indented under the table
- Partition type must be
= calculationGroup (not = m or = calculated)
- Use
formatStringDefinition (not formatString) for calc items that override measure format
Security Roles
role RegionalManager
modelPermission: read
tablePermission Sales = [Region] = "East"
modelPermission: required — use read or readRefresh
- Assign users via Power BI REST API, not TMDL:
POST .../datasets/{id}/users with roles array
- Do NOT use
INFO.ROLES() / INFO.ROLEMEMBERSHIPS() via DAX — unreliable. Use the REST API.
Annotations
- Do NOT add
PBI_* annotations manually — they are Power BI internal metadata
- Custom annotations are fine for documentation/tooling
- Syntax: blank line before the first annotation; blank line between annotations; same indent as peer properties
column 'Product Name'
dataType: string
sourceColumn: Product Name
annotation MyTool_Owner = analytics-team
model.tmdl Required Properties
model.tmdl is the root of the definition/ folder alongside database.tmdl, expressions.tmdl, functions.tmdl, relationships.tmdl, roles/, perspectives/, cultures/, and tables/.
model Model
culture: en-US
defaultPowerBIDataSourceVersion: powerBI_V3
discourageImplicitMeasures
sourceQueryCulture: en-US
dataAccessOptions
legacyRedirects
returnErrorValuesAsNull
defaultPowerBIDataSourceVersion: powerBI_V3 is required for Import-mode models — without it, Import from JSON supported for V3 models only.
Direct Lake Configuration
ALL partitions must use EntityPartitionSource — no M/Power Query
A named expression pointing to the Lakehouse/Warehouse must be defined before tables:
expression DL_Lakehouse =
let
Source = AzureStorage.DataLake("https://onelake.dfs.fabric.microsoft.com/<WorkspaceId>/<LakehouseId>", [HierarchicalNavigation=true])
in
Source
Each table partition references the expression:
partition Sales = entity
mode: directLake
source
entityName: Sales
schemaName: dbo
expressionSource: DL_Lakehouse
dataType: binary columns are NOT supported in Direct Lake
Columns map directly via sourceColumn — no transforms
Gotchas
| Issue |
Cause |
Fix |
InvalidLineType: Property! in database.tmdl |
Bare compatibilityLevel: without database declaration |
Start the file with database <Name> on line 1 |
Import from JSON supported for V3 models only |
Missing defaultPowerBIDataSourceVersion |
Add powerBI_V3 to model.tmdl |
| Spaces-for-tabs validation errors |
Editor converted tabs |
Force literal tabs; configure editor not to expand |
// comment ignored or invalid |
Not supported |
Use /// on line above the object (descriptions only) |
| Measure has wrong inferred type |
dataType was set manually |
Remove dataType from measures — always inferred |
Missing formatString errors |
Measure without formatString |
Always set per measure; use formatStringDefinition for dynamic |
| Calc item format ignored |
Used formatString instead of formatStringDefinition |
formatStringDefinition is DAX-based; only it overrides the selected measure's format |
| Broken report binding after column rename |
Stale lineageTag left in place |
Never edit lineageTag; let Power BI regenerate only on creation |
| Role members ignored |
Authored member statically |
Assign via Power BI REST API (POST datasets/{id}/users) |
INFO.ROLES() returns stale/missing data |
Known DAX surface unreliability |
Query membership via REST API |
| Calendar name collision |
Name unique per-table but not per-model |
Calendar names must be globally unique across the model |
| Direct Lake partition errors |
binary column in source |
Cast away in upstream Lakehouse/Warehouse; drop the column |
| Perspective appears empty in Power BI |
No perspectiveTable children |
Add at least one table + column/measure, or includeAll on a table |
model.bim and definition/ both present |
Forgot to delete .bim after TMDL conversion |
Remove model.bim; they are mutually exclusive |
| TMDL conversion fails |
Old Microsoft.AnalysisServices.retail.amd64 |
Upgrade NuGet package for TmdlSerializer |
| Hierarchy level references missing column |
Column removed or renamed without updating level |
level.column: must reference an existing same-table column |
PBI_* annotation edits revert |
Power BI rewrites on save |
Do not hand-author PBI internal annotations |
Additional reference
- Microsoft Learn: TMDL language overview
- Microsoft Learn: TMDL view in Power BI Desktop
- Microsoft Learn: Power BI Desktop project semantic model folder (PBIP)
- Companion references/REFERENCE.md: per-object property tables (
database, model, table, column, measure, relationship, hierarchy, partition, calculationGroup, role, perspective, cultureInfo, expression, function, dataSource, refreshPolicy, calendar, queryGroup), BIM ↔ TMDL conversion procedure, enum value lists, and a comprehensive MS Learn link bundle (TMDL syntax / TMDL view / PBIP folder / calculation groups / Direct Lake / DAX / TMSL / TOM).
Source: wardawgmalvicious/claude-config — distributed by TomeVault.
1---2name: fabric-tmdl3description: TMDL (Tabular Model Definition Language) authoring rules for Fabric and Power BI semantic models. Use when editing .tmdl files, adding measures or columns to a semantic model, defining relationships or calculation groups, working in a PBIP definition/ folder, configuring Direct Lake partitions, or debugging TMDL validation errors. Covers syntax (tabs not spaces, /// descriptions, single-quoting names), DAX measure patterns, row-level security roles, calendar groups, and common gotchas. Use when this capability is needed.4---56## TMDL Authoring Rules78### Syntax Rules (MUST follow)910- **TMDL uses tab indentation** — every nesting level is exactly one tab (`\t`), NOT spaces. Spaces cause validation errors.11 - PowerShell: use `` `t ``12 - Bash: use `$'\t'` or literal tabs13- Objects declared by type + name: `table Customer`, `column ProductId`, `measure 'Total Sales'`14- Names with spaces or special chars (`.`, `=`, `:`, `'`) must be in **single quotes**: `column 'Order Date'`15- Descriptions use `///` placed ABOVE the object — do NOT use the `description` property16- `//` comments are **NOT supported** in TMDL17- Do NOT add `lineageTag` on new objects — it is auto-generated18- Multi-line DAX must be enclosed in triple backticks (` ``` `)19- Place **measures before columns** in table definitions20- `formatString` is required on every measure21- Never set `dataType` on measures — it is inferred from DAX2223### Naming Conventions2425- **Tables**: business-friendly, no `Fact`/`Dim` prefixes. Plural for facts (`Sales`), singular for dimensions (`Product`)26- **Columns**: readable with spaces (`Order Date`, `Unit Price`)27- **Measures**: clear patterns (`Total Sales`, `# Customers`). Time intelligence: `[measure]`, `[measure (ly)]`, `[measure (ytd)]`)2829### Column Rules3031| Property | Rule |32|---|---|33| `dataType` | Required. Use `int64`, `decimal`, `string`, `dateTime`, `boolean`. Avoid `double` |34| `sourceColumn` | Must match partition source column name exactly |35| `isHidden` | Set for ID columns, foreign keys, system columns |36| `summarizeBy` | `none` for non-aggregatable numerics (IDs, postal codes, year numbers) |37| `isAvailableInMdx` | `false` for hidden columns not used in sort-by or hierarchies |38| `sortByColumn` | For text needing non-alphabetical sort (month names → month number) |3940### Measure & DAX Rules4142- Always set `formatString` — Currency: `$#,##0.00` | Percentage: `0.00%` | Integer: `#,##0` | Decimal: `#,##0.00`43- Use `DIVIDE()` instead of `/` for safe division44- **Never** use `IFERROR` — causes performance degradation45- Prefix `VAR` names with `_`: `VAR _totalSales = ...`46- Use `displayFolder` to organize measures into logical groups47- Add `///` descriptions to explain business logic4849### Relationship Rules5051- `fromColumn:` = many-side (fact); `toColumn:` = one-side (dimension)52- Create relationships BEFORE measures that depend on them53- Default: `crossFilteringBehavior: oneDirection`; add `bothDirections` only when needed54- `isActive: false` for role-playing dimensions; use `USERELATIONSHIP()` in DAX55- Both sides must have matching `dataType`56- Set `isKey: true` on dimension primary key columns57- Hide foreign keys on fact tables (`isHidden: true`)58- No composite keys — use a single surrogate integer key5960### Calculation Groups6162```tmdl63table 'Time Intelligence'64 calculationGroup65 calculationItem Current = SELECTEDMEASURE()66 calculationItem YTD = CALCULATE(SELECTEDMEASURE(), DATESYTD('Date'[Date]))67 column 'Time Intelligence'68 dataType: string69 partition 'Partition_Time Intelligence' = calculationGroup70```7172- `calculationGroup` keyword has NO name — just the keyword indented under the table73- Partition type must be `= calculationGroup` (not `= m` or `= calculated`)74- Use `formatStringDefinition` (not `formatString`) for calc items that override measure format7576### Security Roles7778```tmdl79role RegionalManager80 modelPermission: read81 tablePermission Sales = [Region] = "East"82```8384- `modelPermission:` required — use `read` or `readRefresh`85- Assign users via Power BI REST API, not TMDL: `POST .../datasets/{id}/users` with `roles` array86- Do NOT use `INFO.ROLES()` / `INFO.ROLEMEMBERSHIPS()` via DAX — unreliable. Use the REST API.8788### Annotations8990- Do NOT add `PBI_*` annotations manually — they are Power BI internal metadata91- Custom annotations are fine for documentation/tooling92- Syntax: blank line before the first annotation; blank line between annotations; same indent as peer properties9394```tmdl95column 'Product Name'96 dataType: string97 sourceColumn: Product Name9899 annotation MyTool_Owner = analytics-team100```101102103---104105106## model.tmdl Required Properties107108`model.tmdl` is the root of the `definition/` folder alongside `database.tmdl`, `expressions.tmdl`, `functions.tmdl`, `relationships.tmdl`, `roles/`, `perspectives/`, `cultures/`, and `tables/`.109110```tmdl111model Model112 culture: en-US113 defaultPowerBIDataSourceVersion: powerBI_V3114 discourageImplicitMeasures115 sourceQueryCulture: en-US116 dataAccessOptions117 legacyRedirects118 returnErrorValuesAsNull119```120121`defaultPowerBIDataSourceVersion: powerBI_V3` is required for Import-mode models — without it, `Import from JSON supported for V3 models only`.122123124---125126127## Direct Lake Configuration128129- ALL partitions must use `EntityPartitionSource` — no M/Power Query130- A named expression pointing to the Lakehouse/Warehouse must be defined before tables:131132 ```tmdl133 expression DL_Lakehouse =134 let135 Source = AzureStorage.DataLake("https://onelake.dfs.fabric.microsoft.com/<WorkspaceId>/<LakehouseId>", [HierarchicalNavigation=true])136 in137 Source138 ```139140- Each table partition references the expression:141142 ```tmdl143 partition Sales = entity144 mode: directLake145 source146 entityName: Sales147 schemaName: dbo148 expressionSource: DL_Lakehouse149 ```150151- `dataType: binary` columns are NOT supported in Direct Lake152- Columns map directly via `sourceColumn` — no transforms153154155---156157158## Gotchas159160| Issue | Cause | Fix |161|---|---|---|162| `InvalidLineType: Property!` in `database.tmdl` | Bare `compatibilityLevel:` without `database` declaration | Start the file with `database <Name>` on line 1 |163| `Import from JSON supported for V3 models only` | Missing `defaultPowerBIDataSourceVersion` | Add `powerBI_V3` to `model.tmdl` |164| Spaces-for-tabs validation errors | Editor converted tabs | Force literal tabs; configure editor not to expand |165| `//` comment ignored or invalid | Not supported | Use `///` on line above the object (descriptions only) |166| Measure has wrong inferred type | `dataType` was set manually | Remove `dataType` from measures — always inferred |167| Missing `formatString` errors | Measure without `formatString` | Always set per measure; use `formatStringDefinition` for dynamic |168| Calc item format ignored | Used `formatString` instead of `formatStringDefinition` | `formatStringDefinition` is DAX-based; only it overrides the selected measure's format |169| Broken report binding after column rename | Stale `lineageTag` left in place | Never edit `lineageTag`; let Power BI regenerate only on creation |170| Role members ignored | Authored `member` statically | Assign via Power BI REST API (`POST datasets/{id}/users`) |171| `INFO.ROLES()` returns stale/missing data | Known DAX surface unreliability | Query membership via REST API |172| Calendar name collision | Name unique per-table but not per-model | Calendar names must be globally unique across the model |173| Direct Lake partition errors | `binary` column in source | Cast away in upstream Lakehouse/Warehouse; drop the column |174| Perspective appears empty in Power BI | No `perspectiveTable` children | Add at least one table + column/measure, or `includeAll` on a table |175| `model.bim` and `definition/` both present | Forgot to delete `.bim` after TMDL conversion | Remove `model.bim`; they are mutually exclusive |176| TMDL conversion fails | Old `Microsoft.AnalysisServices.retail.amd64` | Upgrade NuGet package for `TmdlSerializer` |177| Hierarchy level references missing column | Column removed or renamed without updating level | `level.column:` must reference an existing same-table column |178| `PBI_*` annotation edits revert | Power BI rewrites on save | Do not hand-author PBI internal annotations |179180181---182183## Additional reference184185- Microsoft Learn: [TMDL language overview](https://learn.microsoft.com/analysis-services/tmdl/tmdl-overview)186- Microsoft Learn: [TMDL view in Power BI Desktop](https://learn.microsoft.com/power-bi/transform-model/desktop-tmdl-view)187- Microsoft Learn: [Power BI Desktop project semantic model folder (PBIP)](https://learn.microsoft.com/power-bi/developer/projects/projects-dataset)188- Companion [references/REFERENCE.md](references/REFERENCE.md): per-object property tables (`database`, `model`, `table`, `column`, `measure`, `relationship`, `hierarchy`, `partition`, `calculationGroup`, `role`, `perspective`, `cultureInfo`, `expression`, `function`, `dataSource`, `refreshPolicy`, `calendar`, `queryGroup`), BIM ↔ TMDL conversion procedure, enum value lists, and a comprehensive MS Learn link bundle (TMDL syntax / TMDL view / PBIP folder / calculation groups / Direct Lake / DAX / TMSL / TOM).189190---191> Source: [wardawgmalvicious/claude-config](https://github.com/wardawgmalvicious/claude-config) — distributed by [TomeVault](https://tomevault.io).192<!-- tomevault:4.0:skill_md:2026-06-16 -->