Skill Overview
This skill provides production-grade Power BI guidance across five domains:
- Data Modeling — Star Schema, relationships, storage modes
- DAX Optimization — Measures, variables, pattern library
- Dashboard & KPI Design — Visual selection, layout, UX
- Themes & PBIP — JSON theme generation, project structure
- Power Query / M — Query folding, transformation best practices
Available Scripts
| Script |
Purpose |
Run With |
scripts/check_bpa_rules.py |
Audit semantic model for BPA violations |
uv run scripts/check_bpa_rules.py --model <path> |
scripts/validate_theme.py |
Validate report theme JSON structure |
uv run scripts/validate_theme.py --theme <path> |
scripts/generate_theme.py |
Generate a custom report theme JSON |
uv run scripts/generate_theme.py --palette <name> |
scripts/dax_formatter.py |
Format and lint a DAX measure string |
uv run scripts/dax_formatter.py --measure "<DAX>" |
Reference Files
references/dax-patterns.md — Common DAX patterns: YTD, MTD, Rolling, Ranking, Pareto
references/bpa-rules.md — Full Best Practice Analyzer rule catalog with severity ratings
references/theme-schema.md — Power BI report theme JSON schema with all fields documented
references/m-patterns.md — Power Query M patterns: query folding, custom functions, error handling
1. Data Modeling Best Practices
Star Schema (Non-Negotiable)
- Always design around a Star Schema: one or more fact tables at the center, dimension tables surrounding them.
- Avoid Snowflake schemas — they add join complexity with minimal benefit in VertiPaq.
- If you receive a Snowflake model, recommend flattening dimensions with Power Query merges.
Relationships
- Use surrogate integer keys (e.g.,
DateKey INT, ProductKey INT) — never string or float keys.
- Set relationships to single-direction unless cross-filtering is absolutely required.
- Avoid many-to-many relationships on high-cardinality columns. Use a bridge table instead.
- Check for inactive relationships — use
USERELATIONSHIP() in DAX to activate them selectively.
Storage Mode Decision Tree
Is data > 1 billion rows or requires real-time updates?
YES → DirectQuery (with aggregations for performance)
NO → Import Mode (maximum VertiPaq compression and speed)
Is the table a large fact table with daily batch refresh?
YES → Enable Incremental Refresh
NO → Standard scheduled refresh
Cardinality Reduction Checklist
2. DAX Optimization
Core Rules
| ❌ Avoid |
✅ Use Instead |
Reason |
[Sales] / [Cost] |
DIVIDE([Sales], [Cost], 0) |
Handles division by zero gracefully |
IFERROR(...) |
Preventive logic with IF(ISBLANK(...)) |
IFERROR is expensive; it evaluates both branches |
SUMX(Table, ...) for simple totals |
SUM(Table[Column]) |
Iterators have overhead; use aggregators when possible |
INTERSECT for virtual joins |
TREATAS |
TREATAS is more expressive and performant |
| Repeated complex sub-expressions |
VAR x = <expr> RETURN x |
Variables are evaluated once and cached |
CALCULATE(SUM(...), ALL(...)) inline |
Extract to a named base measure |
Readability and reuse |
Variable Pattern (Always Use)
Profit Margin % =
VAR TotalSales = [Total Sales]
VAR TotalCosts = [Total Costs]
VAR Margin = DIVIDE(TotalSales - TotalCosts, TotalSales, 0)
RETURN
IF(TotalSales = 0, BLANK(), Margin)
Time Intelligence Patterns
Always verify a marked Date table exists before applying time intelligence.
-- Year-to-Date
Sales YTD = CALCULATE([Total Sales], DATESYTD('Date'[Date]))
-- Prior Year Comparison
Sales PY = CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date]))
-- Rolling 12 Months
Sales R12M = CALCULATE([Total Sales], DATESINPERIOD('Date'[Date], LASTDATE('Date'[Date]), -12, MONTH))
→ For the full pattern library, read references/dax-patterns.md
3. Dashboard & KPI Design
Page Layout Rules
- Maximum 8 visuals per page — every additional visual adds query overhead ("death by a thousand cuts").
- Use bookmarks to hide/show panels rather than placing everything on screen simultaneously.
- Reserve top 15–20% of the page for a navigation bar and global filters/slicers.
- All visuals on a page must share a coherent data grain — avoid mixing order-level and customer-level visuals.
KPI Card Standard (The "TVGT" Framework)
Every KPI card must include all four elements:
| Element |
Description |
Example |
| Target |
The goal/benchmark |
Budget: $1.2M |
| Value |
Actual current figure |
Actual: $1.05M |
| Gap |
Variance (absolute + %) |
-$150K (-12.5%) |
| Trend |
Direction over time |
Sparkline / up-down arrow |
- Limit to 5 KPI cards per page maximum.
- Color-code with accessible palettes: use Blue/Orange instead of Red/Green (color-blind safe).
- Use conditional formatting for gap columns — never rely on the user reading numbers.
Visual Selection Guide
| Use Case |
Recommended Visual |
Avoid |
| KPI summary |
Native KPI, Card (new), Deneb |
SVG concatenation hacks |
| Time series |
Line chart, Area chart |
Pie charts for time data |
| Ranking / Pareto |
Bar chart + line combo |
3D charts (any) |
| Geo distribution |
Filled map, Azure Maps |
Custom map SVGs |
| Table with variance |
Matrix with conditional formatting |
Plain table without formatting |
| Small multiples |
Small multiples (native) |
Many individual charts |
Certified vs. Custom Visuals
- Prefer native visuals → certified AppSource visuals → Deneb (Vega-Lite) in that order.
- Avoid SVG-DAX string concatenation workarounds — unmaintainable and fragile.
- Never use uncertified visuals in production reports (security and refresh risk).
4. Report Themes & PBIP
Generating a Report Theme
When asked to create a theme, always output a valid JSON following this structure:
{
"name": "Custom Theme Name",
"dataColors": ["#1B6CA8","#E07B39","#2E8B57","#8B2FC9","#C42B1C","#0D7D6C","#A67B2C","#5C5C5C"],
"background": "#FFFFFF",
"foreground": "#252423",
"tableAccent": "#1B6CA8",
"textClasses": {
"callout": { "fontFace": "Segoe UI", "fontSize": 45, "fontColor": "#252423", "bold": true },
"title": { "fontFace": "Segoe UI", "fontSize": 16, "fontColor": "#252423", "bold": true },
"header": { "fontFace": "Segoe UI", "fontSize": 12, "fontColor": "#252423", "bold": true },
"label": { "fontFace": "Segoe UI", "fontSize": 10, "fontColor": "#605E5C" }
},
"visualStyles": {
"*": {
"*": {
"border": [{ "show": false }],
"background": [{ "show": false }],
"dropShadow": [{ "show": false }]
}
},
"tableEx": {
"*": {
"grid": [{ "gridVertical": false, "rowPadding": 6 }],
"columnHeaders": [{ "bold": true, "wordWrap": true }],
"values": [{ "wordWrap": true }]
}
},
"lineChart": {
"*": {
"legend": [{ "position": "Top" }],
"categoryAxis": [{ "showAxisTitle": false }]
}
}
}
}
Use scripts/generate_theme.py to produce full themes programmatically.
PBIP Project Structure
MyReport.pbip
├── MyReport.Report/
│ ├── report.json ← Visual layout (DO NOT hand-edit)
│ ├── definition.pbir ← Report metadata
│ └── StaticResources/
│ └── RegisteredResources/
│ └── theme.json ← ✅ Safe to edit: your custom theme goes here
└── MyReport.SemanticModel/
├── definition.pbism ← Model metadata
├── model.bim ← ✅ Safe to edit: tables, measures, relationships
└── .pbi/
└── localSettings.json ← ❌ Do NOT commit: user-local settings
PBIP Safety Rules:
report.json and diagramLayout.json — never edit manually, high corruption risk.
- Add
.pbi/localSettings.json to .gitignore.
model.bim is safe to edit for adding measures, updating expressions, changing formatting.
- Use Tabular Editor 3 or ALM Toolkit for programmatic model changes.
5. Power Query / M Optimization
Query Folding (Critical)
Query folding pushes transformations back to the data source (SQL Server, etc.), dramatically reducing load on the Power BI engine.
Folding-safe transformations:
- Filter rows, remove columns, rename columns, change data types (native)
- Group by, sort, merge (inner/left joins on indexed columns)
Folding-breaking transformations (use sparingly, move to end):
- Custom columns with M functions,
List.Generate, Table.AddColumn with complex logic
- Always check: right-click a step → "View Native Query" — if grayed out, folding is broken.
M Best Practices
// Good: Folding preserved — filter early
let
Source = Sql.Database("server", "db"),
Sales = Source{[Schema="dbo", Item="Sales"]}[Data],
FilteredRows = Table.SelectRows(Sales, each [Year] = 2024), // ← folds
RemovedCols = Table.SelectColumns(FilteredRows, {"Date","Amount","ProductKey"})
in
RemovedCols
// Bad: Custom column breaks folding for all subsequent steps
AddedColumn = Table.AddColumn(Sales, "Custom", each Text.Upper([Status])) // ← breaks fold
→ For reusable M patterns, read references/m-patterns.md
6. Row-Level Security (RLS)
Static RLS
// In the Region dimension table filter
[RegionCode] = USERNAME()
Dynamic RLS (Recommended for production)
// User mapping table approach
[Email] = USERPRINCIPALNAME()
RLS Checklist:
7. Deployment Checklist
Before publishing to Power BI Service, verify:
1---2name: powerbi-expert3description: Expert Power BI assistant for analyzing, optimizing, designing, and troubleshooting Power BI solutions end-to-end. Use this skill whenever the user mentions Power BI, PBIX, PBIP, DAX, Power Query, M code, semantic models, report themes, KPI dashboards, data models, DirectQuery, Import Mode, VertiPaq, or any Power BI Desktop/Service topic. Trigger even for vague requests like "my dashboard is slow", "fix my DAX formula", "make a better report", "create a theme", "check my data model", "what visuals should I use", or "help me with Power BI". This skill covers: performance tuning, DAX best practices, Star Schema modeling, report theme JSON generation, PBIP project structure, KPI design, Power Query / M optimization, Row-Level Security (RLS), incremental refresh, and deployment pipelines.4---56## Skill Overview78This skill provides production-grade Power BI guidance across five domains:91. **Data Modeling** — Star Schema, relationships, storage modes102. **DAX Optimization** — Measures, variables, pattern library113. **Dashboard & KPI Design** — Visual selection, layout, UX124. **Themes & PBIP** — JSON theme generation, project structure135. **Power Query / M** — Query folding, transformation best practices1415## Available Scripts1617| Script | Purpose | Run With |18|---|---|---|19| `scripts/check_bpa_rules.py` | Audit semantic model for BPA violations | `uv run scripts/check_bpa_rules.py --model <path>` |20| `scripts/validate_theme.py` | Validate report theme JSON structure | `uv run scripts/validate_theme.py --theme <path>` |21| `scripts/generate_theme.py` | Generate a custom report theme JSON | `uv run scripts/generate_theme.py --palette <name>` |22| `scripts/dax_formatter.py` | Format and lint a DAX measure string | `uv run scripts/dax_formatter.py --measure "<DAX>"` |2324## Reference Files2526- `references/dax-patterns.md` — Common DAX patterns: YTD, MTD, Rolling, Ranking, Pareto27- `references/bpa-rules.md` — Full Best Practice Analyzer rule catalog with severity ratings28- `references/theme-schema.md` — Power BI report theme JSON schema with all fields documented29- `references/m-patterns.md` — Power Query M patterns: query folding, custom functions, error handling3031---3233## 1. Data Modeling Best Practices3435### Star Schema (Non-Negotiable)36- **Always** design around a Star Schema: one or more fact tables at the center, dimension tables surrounding them.37- Avoid Snowflake schemas — they add join complexity with minimal benefit in VertiPaq.38- If you receive a Snowflake model, recommend flattening dimensions with Power Query merges.3940### Relationships41- Use **surrogate integer keys** (e.g., `DateKey INT`, `ProductKey INT`) — never string or float keys.42- Set relationships to **single-direction** unless cross-filtering is absolutely required.43- Avoid many-to-many relationships on high-cardinality columns. Use a bridge table instead.44- Check for **inactive relationships** — use `USERELATIONSHIP()` in DAX to activate them selectively.4546### Storage Mode Decision Tree47```48Is data > 1 billion rows or requires real-time updates?49 YES → DirectQuery (with aggregations for performance)50 NO → Import Mode (maximum VertiPaq compression and speed)51 Is the table a large fact table with daily batch refresh?52 YES → Enable Incremental Refresh53 NO → Standard scheduled refresh54```5556### Cardinality Reduction Checklist57- [ ] Disable **Auto Date/Time** (File > Options > Data Load)58- [ ] Create a dedicated **Date table** with `CALENDARAUTO()` or a pre-built template59- [ ] Split DateTime columns into separate **Date** and **Time** columns60- [ ] Replace float/decimal columns with fixed-precision integers where possible61- [ ] Remove unused columns **before** loading (not after — saves VertiPaq memory)62- [ ] Use integer encoding for status/category columns (e.g., `1=Active`, `0=Inactive`)6364---6566## 2. DAX Optimization6768### Core Rules69| ❌ Avoid | ✅ Use Instead | Reason |70|---|---|---|71| `[Sales] / [Cost]` | `DIVIDE([Sales], [Cost], 0)` | Handles division by zero gracefully |72| `IFERROR(...)` | Preventive logic with `IF(ISBLANK(...))` | IFERROR is expensive; it evaluates both branches |73| `SUMX(Table, ...)` for simple totals | `SUM(Table[Column])` | Iterators have overhead; use aggregators when possible |74| `INTERSECT` for virtual joins | `TREATAS` | TREATAS is more expressive and performant |75| Repeated complex sub-expressions | `VAR x = <expr> RETURN x` | Variables are evaluated once and cached |76| `CALCULATE(SUM(...), ALL(...))` inline | Extract to a named base measure | Readability and reuse |7778### Variable Pattern (Always Use)79```dax80Profit Margin % =81VAR TotalSales = [Total Sales]82VAR TotalCosts = [Total Costs]83VAR Margin = DIVIDE(TotalSales - TotalCosts, TotalSales, 0)84RETURN85 IF(TotalSales = 0, BLANK(), Margin)86```8788### Time Intelligence Patterns89Always verify a marked Date table exists before applying time intelligence.90```dax91-- Year-to-Date92Sales YTD = CALCULATE([Total Sales], DATESYTD('Date'[Date]))9394-- Prior Year Comparison95Sales PY = CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date]))9697-- Rolling 12 Months98Sales R12M = CALCULATE([Total Sales], DATESINPERIOD('Date'[Date], LASTDATE('Date'[Date]), -12, MONTH))99```100101→ For the full pattern library, read `references/dax-patterns.md`102103---104105## 3. Dashboard & KPI Design106107### Page Layout Rules108- **Maximum 8 visuals per page** — every additional visual adds query overhead ("death by a thousand cuts").109- Use **bookmarks** to hide/show panels rather than placing everything on screen simultaneously.110- Reserve **top 15–20%** of the page for a navigation bar and global filters/slicers.111- All visuals on a page must share a coherent **data grain** — avoid mixing order-level and customer-level visuals.112113### KPI Card Standard (The "TVGT" Framework)114Every KPI card must include all four elements:115116| Element | Description | Example |117|---|---|---|118| **T**arget | The goal/benchmark | Budget: $1.2M |119| **V**alue | Actual current figure | Actual: $1.05M |120| **G**ap | Variance (absolute + %) | -$150K (-12.5%) |121| **T**rend | Direction over time | Sparkline / up-down arrow |122123- Limit to **5 KPI cards per page** maximum.124- Color-code with **accessible palettes**: use Blue/Orange instead of Red/Green (color-blind safe).125- Use conditional formatting for gap columns — never rely on the user reading numbers.126127### Visual Selection Guide128| Use Case | Recommended Visual | Avoid |129|---|---|---|130| KPI summary | Native KPI, Card (new), Deneb | SVG concatenation hacks |131| Time series | Line chart, Area chart | Pie charts for time data |132| Ranking / Pareto | Bar chart + line combo | 3D charts (any) |133| Geo distribution | Filled map, Azure Maps | Custom map SVGs |134| Table with variance | Matrix with conditional formatting | Plain table without formatting |135| Small multiples | Small multiples (native) | Many individual charts |136137### Certified vs. Custom Visuals138- Prefer **native visuals** → **certified AppSource visuals** → **Deneb (Vega-Lite)** in that order.139- Avoid SVG-DAX string concatenation workarounds — unmaintainable and fragile.140- Never use uncertified visuals in production reports (security and refresh risk).141142---143144## 4. Report Themes & PBIP145146### Generating a Report Theme147When asked to create a theme, always output a valid JSON following this structure:148149```json150{151 "name": "Custom Theme Name",152 "dataColors": ["#1B6CA8","#E07B39","#2E8B57","#8B2FC9","#C42B1C","#0D7D6C","#A67B2C","#5C5C5C"],153 "background": "#FFFFFF",154 "foreground": "#252423",155 "tableAccent": "#1B6CA8",156 "textClasses": {157 "callout": { "fontFace": "Segoe UI", "fontSize": 45, "fontColor": "#252423", "bold": true },158 "title": { "fontFace": "Segoe UI", "fontSize": 16, "fontColor": "#252423", "bold": true },159 "header": { "fontFace": "Segoe UI", "fontSize": 12, "fontColor": "#252423", "bold": true },160 "label": { "fontFace": "Segoe UI", "fontSize": 10, "fontColor": "#605E5C" }161 },162 "visualStyles": {163 "*": {164 "*": {165 "border": [{ "show": false }],166 "background": [{ "show": false }],167 "dropShadow": [{ "show": false }]168 }169 },170 "tableEx": {171 "*": {172 "grid": [{ "gridVertical": false, "rowPadding": 6 }],173 "columnHeaders": [{ "bold": true, "wordWrap": true }],174 "values": [{ "wordWrap": true }]175 }176 },177 "lineChart": {178 "*": {179 "legend": [{ "position": "Top" }],180 "categoryAxis": [{ "showAxisTitle": false }]181 }182 }183 }184}185```186187Use `scripts/generate_theme.py` to produce full themes programmatically.188189### PBIP Project Structure190```191MyReport.pbip192├── MyReport.Report/193│ ├── report.json ← Visual layout (DO NOT hand-edit)194│ ├── definition.pbir ← Report metadata195│ └── StaticResources/196│ └── RegisteredResources/197│ └── theme.json ← ✅ Safe to edit: your custom theme goes here198└── MyReport.SemanticModel/199 ├── definition.pbism ← Model metadata200 ├── model.bim ← ✅ Safe to edit: tables, measures, relationships201 └── .pbi/202 └── localSettings.json ← ❌ Do NOT commit: user-local settings203```204205**PBIP Safety Rules:**206- `report.json` and `diagramLayout.json` — **never edit manually**, high corruption risk.207- Add `.pbi/localSettings.json` to `.gitignore`.208- `model.bim` is safe to edit for adding measures, updating expressions, changing formatting.209- Use **Tabular Editor 3** or **ALM Toolkit** for programmatic model changes.210211---212213## 5. Power Query / M Optimization214215### Query Folding (Critical)216Query folding pushes transformations back to the data source (SQL Server, etc.), dramatically reducing load on the Power BI engine.217218**Folding-safe transformations:**219- Filter rows, remove columns, rename columns, change data types (native)220- Group by, sort, merge (inner/left joins on indexed columns)221222**Folding-breaking transformations (use sparingly, move to end):**223- Custom columns with M functions, `List.Generate`, `Table.AddColumn` with complex logic224- Always check: right-click a step → "View Native Query" — if grayed out, folding is broken.225226### M Best Practices227```m228// Good: Folding preserved — filter early229let230 Source = Sql.Database("server", "db"),231 Sales = Source{[Schema="dbo", Item="Sales"]}[Data],232 FilteredRows = Table.SelectRows(Sales, each [Year] = 2024), // ← folds233 RemovedCols = Table.SelectColumns(FilteredRows, {"Date","Amount","ProductKey"})234in235 RemovedCols236237// Bad: Custom column breaks folding for all subsequent steps238AddedColumn = Table.AddColumn(Sales, "Custom", each Text.Upper([Status])) // ← breaks fold239```240241→ For reusable M patterns, read `references/m-patterns.md`242243---244245## 6. Row-Level Security (RLS)246247### Static RLS248```dax249// In the Region dimension table filter250[RegionCode] = USERNAME()251```252253### Dynamic RLS (Recommended for production)254```dax255// User mapping table approach256[Email] = USERPRINCIPALNAME()257```258259**RLS Checklist:**260- [ ] Test with "View as Role" in Power BI Desktop before publishing.261- [ ] Ensure RLS filters propagate correctly across **all** relationships from the filtered table.262- [ ] Do not apply RLS on the fact table directly — filter via dimension tables to leverage relationship propagation.263- [ ] Use Object-Level Security (OLS) in Tabular Editor to hide sensitive columns entirely.264265---266267## 7. Deployment Checklist268269Before publishing to Power BI Service, verify:270- [ ] All measures use `VAR` for complex expressions271- [ ] `DIVIDE()` used everywhere instead of `/`272- [ ] No bi-directional relationships on high-cardinality columns273- [ ] Date table is marked as a Date table (`Mark as Date Table`)274- [ ] Auto Date/Time is disabled275- [ ] Report theme JSON is embedded or linked from `StaticResources/`276- [ ] All visuals are certified or native277- [ ] RLS roles tested with "View as Role"278- [ ] Incremental refresh configured for tables > 1M rows279- [ ] Sensitivity labels applied (if using Microsoft Purview / Information Protection)280- [ ] Gateway configured for on-premises data sources281