
Powerbi Modeling
- 24 installs
- 7 repo stars
- Updated August 2, 2026
- practicalswan/agent-skills
powerbi-modeling is a Claude Code skill for ai & agent building.
About
powerbi-modeling is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- powerbi-modeling
- AI & Agent Building
- AI-coding skill
Powerbi Modeling by the numbers
- 24 all-time installs (skills.sh)
- Ranked #9,912 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/practicalswan/agent-skills --skill powerbi-modelingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 24 |
|---|---|
| repo stars | ★ 7 |
| Last updated | August 2, 2026 |
| Repository | practicalswan/agent-skills ↗ |
How do I helps with ai & agent building tasks.?
Helps with ai & agent building tasks.
Who is it for?
Best when you're working on ai & agent building and need structured help with powerbi modeling.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks., or when powerbi-modeling is a claude code skill for ai & agent building.
What you get
Structured output aligned to powerbi-modeling: powerbi-modeling, AI & Agent Building.
Files
Power BI Modeling
Tech Stack Target / Version: Power BI Desktop current release, Tabular Editor, DAX Studio, and Fabric semantic-model workflows.
Use this skill when the work is inside a Power BI semantic model rather than a generic SQL schema or spreadsheet.
- Leverage native parallel subagent dispatch and 200k+ context windows where available.
Activation Conditions
Use symptom -> action triggers: when one matches, apply this skill and verify with the protocol below.
- Designing or cleaning up a star schema
- Creating or reviewing DAX measures
- Configuring relationships and cross-filter direction
- Implementing row-level security
- Auditing model health and performance
Practical Workflow
1. Inspect the current model before changing anything. 2. Classify tables as dimension, fact, bridge, or helper tables. 3. Prefer explicit measures over implicit aggregation. 4. Keep relationships simple and single-direction unless the use case is proven. 5. Hide technical fields from report authors.
MCP Reality
Power BI model tooling is host-specific. If your client exposes a Power BI modeling MCP server, inspect the available operations first and map them to the model areas you need: connections, tables, columns, measures, relationships, DAX queries, and security roles.
For Microsoft documentation, the Microsoft Learn MCP server is a good companion. Prefer:
microsoft_docs_search_by_productwithpower-bimicrosoft_docs_fetchfor the final page
Anti-Patterns
- Treating source content as already clean: Formatting automation will happily preserve broken or inconsistent input.
- Skipping an open-file verification pass: Documents and spreadsheets often fail in the destination app, not in the script output.
- Automating irreversible edits without checkpoints: A small mapping mistake can affect an entire workbook or document.
Verification Protocol
Before claiming "skill applied successfully":
1. Pass/fail: The Powerbi Modeling implementation names the target runtime, framework version, and affected files. 2. Pass/fail: Build, lint, test, or equivalent local validation is run for the changed surface. 3. Pass/fail: Edge cases for errors, dependency drift, and environment differences are addressed or explicitly out of scope. 4. Pressure-test scenario: Apply the workflow to a change that passes happy-path tests but fails one boundary condition. 5. Success metric: Zero untested success claims; every implementation claim maps to a command or artifact.
References & Resources
Documentation
- STAR-SCHEMA - Dimension and fact modeling guidance
- RELATIONSHIPS - Relationship patterns and cross-filter tradeoffs
- MEASURES-DAX - DAX naming and measure design
- PERFORMANCE - High-impact optimization ideas
- RLS - Row-level security patterns
Scripts
- Power BI Model Audit - Local audit helper for naming, documentation, and modeling smells
Examples
- Model Examples - Example modeling patterns and DAX structure
<!-- PORTABILITY:START -->
Cross-Client Portability
This skill is written to stay usable across GitHub Copilot, Claude Code, Codex, and Gemini CLI.
- GitHub Copilot: keep the folder in a Copilot-visible skill or plugin path, or wrap the workflow as project instructions if the host does not support portable skill folders directly.
- Claude Code: keep the folder in a local skills directory or a compatible plugin or marketplace source.
- Codex: install or sync the folder into
$CODEX_HOME/skills/<skill-name>and restart Codex after major changes. - Gemini CLI: this repository generates a project command named
/skills:powerbi-modelingfrom this skill. Rebuild commands withpython scripts/export-gemini-skill.py powerbi-modelingand then run/commands reloadinside Gemini CLI.
<!-- PORTABILITY:END -->
<!-- MCP:START -->
MCP Availability And Fallback
Preferred MCP Server: Power BI MCP
- Fallback prompt: "Use the Power BI Modeling skill without MCP. Rely on the local
SKILL.md, bundled references or scripts, and manual verification. Show the exact commands, evidence, and final checks you used before concluding." - Use Power BI Desktop, Tabular Editor, DAX Studio, and exported model metadata when the MCP surface is unavailable.
- Validate measures, relationships, and performance with local model tools before completion.
<!-- MCP:END -->
Related Skills
- documentation-authoring: Use it when the workflow also needs drafting structured technical or product documents.
- notion-docs: Use it when the workflow also needs Notion page and database publishing workflows.
- pdf: Use it when the workflow also needs PDF extraction, generation, and layout-aware review.
- word-document: Use it when the workflow also needs Word document authoring and formatting workflows.
Changelog
[2026-04-25] - Version 1.2 Verification Protocol Refresh
Added
- Added a
Verification Protocolsection with skill-specific pass/fail checks, one pressure-test scenario, and a measurable success metric. - Added guidance to leverage native parallel subagent dispatch and 200k+ context windows where available.
Changed
- Updated
SKILL.mdfrontmatter toversion: "1.2"andlast_updated: 2026-04-25. - Reframed activation guidance toward symptom -> action triggers and standardized two-stage review wording where applicable.
[2026-04-24] - Version 1.1 Refresh
Changed
- Updated the SKILL frontmatter version to
1.1for the 2026-04-24 catalog refresh.
All notable changes to this skill will be documented in this file.
[2026-04-24] - Skill Refresh
Changed
- Standardized the SKILL frontmatter with version metadata, last-updated date, tags, and a concise catalog description.
- Reformatted the portability and MCP guidance with a preferred server line, a copy-paste fallback prompt, and consistent bullet lists.
- Added a catalog-standard Anti-Patterns section and refreshed the Related Skills links at the end of the skill.
- Added a Tech Stack Target / Version note so the modeling guidance is anchored to current Power BI and DAX tooling.
[2026-04-04] - Cross-Client Portability Refresh
Changed
- Added a standard portability note covering GitHub Copilot, Claude Code, Codex, and Gemini CLI.
- Documented the preferred MCP server surface for this skill and a local no-MCP fallback workflow.
Tested
- Validated
SKILL.mdfrontmatter, portability sections, and Gemini export readiness withpython scripts/validate-skills.py.
[2026-03-09] - Workspace Modernization
Changed
- Rewrote the skill to treat Power BI modeling MCP access as host-specific and to pair it explicitly with Microsoft Learn documentation lookups
[2026-02-28] — Description Rewrite & Cross-References
Changed
- Rewrote skill description to ~200 characters with clear, specific activation keywords
- Improved keyword specificity to reduce overlap with related skills
Added
## Related Skillscross-reference table with 2-4 related skills and "Use When" guidance
Power BI Modeling Examples
Example 1: Creating a Star Schema
When designing a new data model from scratch:
# Step 1: Connect to the model
connection_operations(operation="Connect")
# Step 2: Create dimension table (Customers)
table_operations(
operation="Create",
name="Customer",
columns=[
{"name": "CustomerKey", "dataType": "Int64"},
{"name": "FirstName", "dataType": "String"},
{"name": "LastName", "dataType": "String"},
{"name": "Email", "dataType": "String"},
{"name": "City", "dataType": "String"},
{"name": "Country", "dataType": "String"}
]
)
# Step 3: Create fact table (Sales)
table_operations(
operation="Create",
name="Sales",
columns=[
{"name": "SalesKey", "dataType": "Int64"},
{"name": "CustomerKey", "dataType": "Int64"},
{"name": "ProductKey", "dataType": "Int64"},
{"name": "DateKey", "dataType": "Int64"},
{"name": "Quantity", "dataType": "Int64"},
{"name": "Amount", "dataType": "Decimal"},
{"name": "Profit", "dataType": "Decimal"}
]
)
# Step 4: Create relationship (one-to-many, single direction)
relationship_operations(
operation="Create",
definitions=[{
"fromTable": "Customer",
"fromColumn": "CustomerKey",
"toTable": "Sales",
"toColumn": "CustomerKey",
"crossFilteringBehavior": "OneDirection"
}]
)Example 2: Adding DAX Measures
When adding business calculations to a model:
# Add basic sum measure
measure_operations(
operation="Create",
definitions=[{
"name": "Total Sales",
"tableName": "Sales",
"expression": "SUM(Sales[Amount])",
"formatString": "$#,##0",
"description": "Sum of all sales amounts"
}]
)
# Add year-to-date measure
measure_operations(
operation="Create",
definitions=[{
"name": "Total Sales YTD",
"tableName": "Sales",
"expression": "CALCULATE([Total Sales], DATESYTD('Date'[Date]))",
"formatString": "$#,##0",
"description": "Year-to-date sales"
}]
)
# Add YoY growth measure
measure_operations(
operation="Create",
definitions=[{
"name": "Sales YoY Growth %",
"tableName": "Sales",
"expression": "DIVIDE([Total Sales] - CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date])), CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date])))",
"formatString": "0.00%",
"description": "Year-over-year sales growth percentage"
}]
)Example 3: Documenting a Model
When adding documentation to improve model usability:
# Update table descriptions
table_operations(
operation="Update",
name="Customer",
description="Customer dimension containing demographic and geographic information"
)
# Update column descriptions
column_operations(
operation="Update",
definitions=[{
"tableName": "Customer",
"name": "CustomerKey",
"description": "Unique identifier for customer (surrogate key)",
"isHidden": True
}, {
"tableName": "Customer",
"name": "Email",
"description": "Primary email address for customer communications"
}]
)
# Update measure descriptions
measure_operations(
operation="Update",
definitions=[{
"name": "Total Sales",
"tableName": "Sales",
"description": "Total revenue from all completed sales transactions"
}]
)Example 4: Implementing Row-Level Security
When adding security roles to restrict data access:
# Create sales manager role
security_role_operations(
operation="Create",
name="Sales Manager",
# Restricts to their region's customers
modelRole=[{
"tableName": "Sales",
"filterExpression": "USERNAME() = [ManagerEmail]"
}]
)
# Create executive role (all data)
security_role_operations(
operation="Create",
name="Executive",
modelRole=[{
"tableName": "Sales",
"filterExpression": "TRUE()" # No filtering
}]
)Example 5: Optimizing Model Performance
When improving performance of existing measures:
# Before: Inefficient measure scanning entire table
measure_operations(
operation="Update",
definitions=[{
"name": "Total Sales",
"tableName": "Sales",
"expression": "SUM(Sales[Amount]) # Scans all rows"
}]
)
# After: Optimized with DIVIDE and error handling
measure_operations(
operation="Update",
definitions=[{
"name": "Total Sales",
"tableName": "Sales",
"expression": "SUMX(RELATEDTABLE('Customer'), CALCULATE(SUM(Sales[Amount])))",
"description": "Optimized: Uses SUMX with filtered context for better performance"
}]
)
# Add hidden technical measures for calculations
measure_operations(
operation="Create",
definitions=[{
"name": "_Total Sales Base",
"tableName": "Sales",
"expression": "SUM(Sales[Amount])",
"isHidden": True,
"description": "Base calculation used by other measures"
}]
)MIT License
Copyright (c) 2026 Sithu Win San
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
DAX Measures and Naming Conventions
Naming Conventions
General Rules
- Use human-readable names (spaces allowed)
- Be descriptive:
Total Sales AmountnotTSA - Avoid abbreviations unless universally understood
- Use consistent capitalization (Title Case recommended)
- Avoid special characters except spaces
Table Naming
| Type | Convention | Example |
|---|---|---|
| Dimension | Singular noun | Customer, Product, Date |
| Fact | Business process | Sales, Orders, Inventory |
| Bridge | Combined names | CustomerAccount, ProductCategory |
| Measure Table | Underscore prefix | _Measures, _KPIs |
Column Naming
| Type | Convention | Example |
|---|---|---|
| Keys | Suffix with "Key" or "ID" | CustomerKey, ProductID |
| Dates | Suffix with "Date" | OrderDate, ShipDate |
| Amounts | Descriptive with unit hint | SalesAmount, QuantitySold |
| Flags | Prefix with "Is" or "Has" | IsActive, HasDiscount |
Measure Naming
| Type | Convention | Example |
|---|---|---|
| Aggregations | Verb + Noun | Total Sales, Count of Orders |
| Ratios | X per Y or X Rate | Sales per Customer, Conversion Rate |
| Time Intelligence | Period + Metric | YTD Sales, PY Total Sales |
| Comparisons | Metric + vs + Baseline | Sales vs Budget, Growth vs PY |
Explicit vs Implicit Measures
Always Create Explicit Measures For:
1. Key business metrics users will query 2. Complex calculations with filter manipulation 3. Measures used in MDX (Excel PivotTables) 4. Controlled aggregation (prevent sum of averages)
Implicit Measures (Column Aggregations)
- Acceptable for simple exploration
- Set correct SummarizeBy property:
- Amounts: Sum
- Keys/IDs: None (Do Not Summarize)
- Rates/Prices: None or Average
Measure Patterns
Basic Aggregations
Total Sales = SUM(Sales[SalesAmount])
Order Count = COUNTROWS(Sales)
Average Order Value = DIVIDE([Total Sales], [Order Count])
Distinct Customers = DISTINCTCOUNT(Sales[CustomerKey])Time Intelligence (Requires Date Table)
YTD Sales = TOTALYTD([Total Sales], 'Date'[Date])
MTD Sales = TOTALMTD([Total Sales], 'Date'[Date])
PY Sales = CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date]))
YoY Growth = DIVIDE([Total Sales] - [PY Sales], [PY Sales])Percentage Calculations
Sales % of Total =
DIVIDE(
[Total Sales],
CALCULATE([Total Sales], REMOVEFILTERS(Product))
)
Margin % = DIVIDE([Gross Profit], [Total Sales])Running Totals
Running Total =
CALCULATE(
[Total Sales],
FILTER(
ALL('Date'),
'Date'[Date] <= MAX('Date'[Date])
)
)Column References
Best Practice: Always Qualify Column Names
// GOOD - Fully qualified
Sales Amount = SUM(Sales[SalesAmount])
// BAD - Unqualified (can cause ambiguity)
Sales Amount = SUM([SalesAmount])Measure References: Never Qualify
// GOOD - Unqualified measure
YTD Sales = TOTALYTD([Total Sales], 'Date'[Date])
// BAD - Qualified measure (breaks if home table changes)
YTD Sales = TOTALYTD(Sales[Total Sales], 'Date'[Date])Documentation
Measure Descriptions
Always add descriptions explaining:
- What the measure calculates
- Business context/usage
- Any important assumptions
measure_operations(
operation: "Update",
definitions: [{
name: "Total Sales",
tableName: "Sales",
description: "Sum of all completed sales transactions. Excludes returns and cancelled orders."
}]
)Format Strings
| Data Type | Format String | Example Output |
|---|---|---|
| Currency | $#,##0.00 | $1,234.56 |
| Percentage | 0.0% | 12.3% |
| Whole Number | #,##0 | 1,234 |
| Decimal | #,##0.00 | 1,234.56 |
Display Folders
Organize measures into logical groups:
measure_operations(
operation: "Update",
definitions: [{
name: "YTD Sales",
tableName: "_Measures",
displayFolder: "Time Intelligence\\Year"
}]
)Common folder structure:
_Measures
├── Sales
│ ├── Total Sales
│ └── Average Sale
├── Time Intelligence
│ ├── Year
│ │ ├── YTD Sales
│ │ └── PY Sales
│ └── Month
│ └── MTD Sales
└── Ratios
├── Margin %
└── Conversion RateVariables for Performance
Use variables to:
- Avoid recalculating the same expression
- Improve readability
- Enable debugging
Gross Margin % =
VAR TotalSales = [Total Sales]
VAR TotalCost = [Total Cost]
VAR GrossProfit = TotalSales - TotalCost
RETURN
DIVIDE(GrossProfit, TotalSales)Validation Checklist
- [ ] All key business metrics have explicit measures
- [ ] Measures have clear, descriptive names
- [ ] Measures have descriptions
- [ ] Appropriate format strings applied
- [ ] Display folders organize related measures
- [ ] Column references are fully qualified
- [ ] Measure references are not qualified
- [ ] Variables used for complex calculations
Performance Optimization for Power BI Models
Data Reduction Techniques
1. Remove Unnecessary Columns
- Only import columns needed for reporting
- Remove audit columns (CreatedBy, ModifiedDate) unless required
- Remove duplicate/redundant columns
column_operations(operation: "List", filter: { tableNames: ["Sales"] })
// Review and remove unneeded columns2. Remove Unnecessary Rows
- Filter historical data to relevant period
- Exclude cancelled/void transactions if not needed
- Apply filters in Power Query (not in DAX)
3. Reduce Cardinality
High cardinality (many unique values) impacts:
- Model size
- Refresh time
- Query performance
Solutions:
| Column Type | Reduction Technique |
|---|---|
| DateTime | Split into Date and Time columns |
| Decimal precision | Round to needed precision |
| Text with patterns | Extract common prefix/suffix |
| High-precision IDs | Use surrogate integer keys |
4. Optimize Data Types
| From | To | Benefit |
|---|---|---|
| DateTime | Date (if time not needed) | 8 bytes to 4 bytes |
| Decimal | Fixed Decimal | Better compression |
| Text with numbers | Whole Number | Much better compression |
| Long text | Shorter text | Reduces storage |
5. Group and Summarize
Pre-aggregate data when detail not needed:
- Daily instead of transactional
- Monthly instead of daily
- Consider aggregation tables for DirectQuery
Column Optimization
Prefer Power Query Columns Over Calculated Columns
| Approach | When to Use |
|---|---|
| Power Query (M) | Can be computed at source, static values |
| Calculated Column (DAX) | Needs model relationships, dynamic logic |
Power Query columns:
- Load faster
- Compress better
- Use less memory
Avoid Calculated Columns on Relationship Keys
DAX calculated columns in relationships:
- Cannot use indexes
- Generate complex SQL for DirectQuery
- Hurt performance significantly
Use COMBINEVALUES for multi-column relationships:
// If you must use calculated column for composite key
CompositeKey = COMBINEVALUES(",", [Country], [City])Set Appropriate Summarization
Prevent accidental aggregation of non-additive columns:
column_operations(
operation: "Update",
definitions: [{
tableName: "Product",
name: "UnitPrice",
summarizeBy: "None"
}]
)Relationship Optimization
1. Minimize Bidirectional Relationships
Each bidirectional relationship:
- Increases query complexity
- Can create ambiguous paths
- Reduces performance
2. Avoid Many-to-Many When Possible
Many-to-many relationships:
- Generate more complex queries
- Require more memory
- Can produce unexpected results
3. Reduce Relationship Cardinality
Keep relationship columns low cardinality:
- Use integer keys over text
- Consider higher-grain relationships
DAX Optimization
1. Use Variables
// GOOD - Calculate once, use twice
Sales Growth =
VAR CurrentSales = [Total Sales]
VAR PriorSales = [PY Sales]
RETURN DIVIDE(CurrentSales - PriorSales, PriorSales)
// BAD - Recalculates [Total Sales] and [PY Sales]
Sales Growth =
DIVIDE([Total Sales] - [PY Sales], [PY Sales])2. Avoid FILTER with Entire Tables
// BAD - Iterates entire table
Sales High Value =
CALCULATE([Total Sales], FILTER(Sales, Sales[Amount] > 1000))
// GOOD - Uses column reference
Sales High Value =
CALCULATE([Total Sales], Sales[Amount] > 1000)3. Use KEEPFILTERS Appropriately
// Respects existing filters
Sales with Filter =
CALCULATE([Total Sales], KEEPFILTERS(Product[Category] = "Bikes"))4. Prefer DIVIDE Over Division Operator
// GOOD - Handles divide by zero
Margin % = DIVIDE([Profit], [Sales])
// BAD - Errors on zero
Margin % = [Profit] / [Sales]DirectQuery Optimization
1. Minimize Columns and Tables
DirectQuery models:
- Query source for every visual
- Performance depends on source
- Minimize data retrieved
2. Avoid Complex Power Query Transformations
- Transforms become subqueries
- Native queries are faster
- Materialize at source when possible
3. Keep Measures Simple Initially
Complex DAX generates complex SQL:
- Start with basic aggregations
- Add complexity gradually
- Monitor query performance
4. Disable Auto Date/Time
For DirectQuery models, disable auto date/time:
- Creates hidden calculated tables
- Increases model complexity
- Use explicit date table instead
Aggregations
User-Defined Aggregations
Pre-aggregate fact tables for:
- Very large models (billions of rows)
- Hybrid DirectQuery/Import
- Common query patterns
table_operations(
operation: "Create",
definitions: [{
name: "SalesAgg",
mode: "Import",
mExpression: "..."
}]
)Performance Testing
Use Performance Analyzer
1. Enable in Power BI Desktop 2. Start recording 3. Interact with visuals 4. Review DAX query times
Monitor with DAX Studio
External tool for:
- Query timing
- Server timings
- Query plans
Validation Checklist
- [ ] Unnecessary columns removed
- [ ] Appropriate data types used
- [ ] High-cardinality columns addressed
- [ ] Bidirectional relationships minimized
- [ ] DAX uses variables for repeated expressions
- [ ] No FILTER on entire tables
- [ ] DIVIDE used instead of division operator
- [ ] Auto date/time disabled for DirectQuery
- [ ] Performance tested with representative data
Relationships in Power BI
Relationship Properties
Cardinality
| Type | Use Case | Notes |
|---|---|---|
| One-to-Many (*:1) | Dimension to Fact | Most common, preferred |
| Many-to-One (1:*) | Fact to Dimension | Same as above, direction reversed |
| One-to-One (1:1) | Dimension extensions | Use sparingly |
| Many-to-Many (:) | Bridge tables, complex scenarios | Requires careful design |
Cross-Filter Direction
| Setting | Behavior | When to Use |
|---|---|---|
| Single | Filters flow from "one" to "many" | Default, best performance |
| Both | Filters flow in both directions | Only when necessary |
Best Practices
1. Prefer One-to-Many Relationships
Customer (1) --> (*) Sales
Product (1) --> (*) Sales
Date (1) --> (*) Sales2. Use Single-Direction Cross-Filtering
Bidirectional filtering:
- Impacts performance negatively
- Can create ambiguous filter paths
- May produce unexpected results
Only use bidirectional when:
- Dimension-to-dimension analysis through fact table
- Specific RLS requirements
Better alternative: Use CROSSFILTER in DAX measures:
Countries Sold =
CALCULATE(
DISTINCTCOUNT(Customer[Country]),
CROSSFILTER(Customer[CustomerKey], Sales[CustomerKey], BOTH)
)3. One Active Path Between Tables
- Only one active relationship between any two tables
- Use USERELATIONSHIP for role-playing dimensions:
Sales by Ship Date =
CALCULATE(
[Total Sales],
USERELATIONSHIP(Sales[ShipDate], Date[Date])
)4. Avoid Ambiguous Paths
Circular references cause errors. Solutions:
- Deactivate one relationship
- Restructure model
- Use USERELATIONSHIP in measures
Relationship Patterns
Standard Star Schema
[Date]
|
[Product]--[Sales]--[Customer]
|
[Store]Role-Playing Dimension
[Date] --(active)-- [Sales.OrderDate]
|
+--(inactive)-- [Sales.ShipDate]Bridge Table (Many-to-Many)
[Customer]--(*)--[CustomerAccount]--(*)--[Account]Factless Fact Table
[Product]--[ProductPromotion]--[Promotion]Used to capture relationships without measures.
Creating Relationships via MCP
List Current Relationships
relationship_operations(operation: "List")Create New Relationship
relationship_operations(
operation: "Create",
definitions: [{
fromTable: "Sales",
fromColumn: "ProductKey",
toTable: "Product",
toColumn: "ProductKey",
crossFilteringBehavior: "OneDirection",
isActive: true
}]
)Deactivate Relationship
relationship_operations(
operation: "Deactivate",
references: [{ name: "relationship-guid-here" }]
)Troubleshooting
"Ambiguous Path" Error
Multiple active paths exist between tables.
- Check for: Multiple fact tables sharing dimensions
- Solution: Deactivate redundant relationships
Bidirectional Not Allowed
Circular reference would be created.
- Solution: Restructure or use DAX CROSSFILTER
Relationship Not Detected
Columns may have different data types.
- Ensure both columns have identical types
- Check for trailing spaces in text keys
Validation Checklist
- [ ] All relationships are one-to-many where possible
- [ ] Cross-filter is single direction by default
- [ ] Only one active path between any two tables
- [ ] Role-playing dimensions use inactive relationships
- [ ] No circular reference paths
- [ ] Key columns have matching data types
Row-Level Security (RLS) in Power BI
Overview
Row-Level Security restricts data access at the row level based on user identity. Users see only the data they're authorized to view.
Design Principles
1. Filter on Dimension Tables
Apply RLS to dimensions, not fact tables:
- More efficient (smaller tables)
- Filters propagate through relationships
- Easier to maintain
// On Customer dimension - filters propagate to Sales
[Region] = "West"2. Create Minimal Roles
Avoid many role combinations:
- Each role = separate cache
- Roles are additive (union, not intersection)
- Consolidate where possible
3. Use Dynamic RLS When Possible
Data-driven rules scale better:
- User mapping in a table
- USERPRINCIPALNAME() for identity
- No role changes when users change
Static vs Dynamic RLS
Static RLS
Fixed rules per role:
// Role: West Region
[Region] = "West"
// Role: East Region
[Region] = "East"Pros: Simple, clear Cons: Doesn't scale, requires role per group
Dynamic RLS
User identity drives filtering:
// Single role filters based on logged-in user
[ManagerEmail] = USERPRINCIPALNAME()Pros: Scales, self-maintaining Cons: Requires user mapping data
Implementation Patterns
Pattern 1: Direct User Mapping
User email in dimension table:
// On Customer table
[CustomerEmail] = USERPRINCIPALNAME()Pattern 2: Security Table
Separate table mapping users to data:
SecurityMapping table:
| UserEmail | Region |
|-----------|--------|
| joe@co.com | West |
| sue@co.com | East |// On Region dimension
[Region] IN
SELECTCOLUMNS(
FILTER(SecurityMapping, [UserEmail] = USERPRINCIPALNAME()),
"Region", [Region]
)Pattern 3: Manager Hierarchy
Users see their data plus subordinates:
// Using PATH functions for hierarchy
PATHCONTAINS(Employee[ManagerPath],
LOOKUPVALUE(Employee[EmployeeID], Employee[Email], USERPRINCIPALNAME()))Pattern 4: Multiple Rules
Combine conditions:
// Users see their region OR if they're a global viewer
[Region] = LOOKUPVALUE(Users[Region], Users[Email], USERPRINCIPALNAME())
|| LOOKUPVALUE(Users[IsGlobal], Users[Email], USERPRINCIPALNAME()) = TRUE()Creating Roles via MCP
List Existing Roles
security_role_operations(operation: "List")Create Role with Permission
security_role_operations(
operation: "Create",
definitions: [{
name: "Regional Sales",
modelPermission: "Read",
description: "Restricts sales data by region"
}]
)Add Table Permission (Filter)
security_role_operations(
operation: "CreatePermissions",
permissionDefinitions: [{
roleName: "Regional Sales",
tableName: "Customer",
filterExpression: "[Region] = USERPRINCIPALNAME()"
}]
)Get Effective Permissions
security_role_operations(
operation: "GetEffectivePermissions",
references: [{ name: "Regional Sales" }]
)Testing RLS
In Power BI Desktop
1. Modeling tab > View As 2. Select role(s) to test 3. Optionally specify user identity 4. Verify data filtering
Test Unexpected Values
For dynamic RLS, test:
- Valid users
- Unknown users (should see nothing or error gracefully)
- NULL/blank values
// Defensive pattern - returns no data for unknown users
IF(
USERPRINCIPALNAME() IN VALUES(SecurityMapping[UserEmail]),
[Region] IN SELECTCOLUMNS(...),
FALSE()
)Common Mistakes
1. RLS on Fact Tables Only
Problem: Large table scans, poor performance Solution: Apply to dimension tables, let relationships propagate
2. Using LOOKUPVALUE Instead of Relationships
Problem: Expensive, doesn't scale Solution: Create proper relationships, let filters flow
3. Expecting Intersection Behavior
Problem: Multiple roles = UNION (additive), not intersection Solution: Design roles with union behavior in mind
4. Forgetting About DirectQuery
Problem: RLS filters become WHERE clauses Solution: Ensure source database can handle the query patterns
5. Not Testing Edge Cases
Problem: Users see unexpected data Solution: Test with: valid users, invalid users, multiple roles
Bidirectional RLS
For bidirectional relationships with RLS:
Enable "Apply security filter in both directions"Only use when:
- RLS requires filtering through many-to-many
- Dimension-to-dimension security needed
Caution: Only one bidirectional relationship per path allowed.
Performance Considerations
- RLS adds WHERE clauses to every query
- Complex DAX in filters hurts performance
- Test with realistic user counts
- Consider aggregations for large models
Object-Level Security (OLS)
Restrict access to entire tables or columns:
// Via XMLA/TMSL - not available in Desktop UIUse for:
- Hiding sensitive columns (salary, SSN)
- Restricting entire tables
- Combined with RLS for comprehensive security
Validation Checklist
- [ ] RLS applied to dimension tables (not fact tables)
- [ ] Filters propagate correctly through relationships
- [ ] Dynamic RLS uses USERPRINCIPALNAME()
- [ ] Tested with valid and invalid users
- [ ] Edge cases handled (NULL, unknown users)
- [ ] Performance tested under load
- [ ] Role mappings documented
- [ ] Workspace roles understood (Admins bypass RLS)
Star Schema Design for Power BI
Overview
Star schema is the optimal design pattern for Power BI semantic models. It organizes data into:
- Dimension tables: Enable filtering and grouping (the "one" side)
- Fact tables: Enable summarization (the "many" side)
Table Classification
Dimension Tables
- Contain descriptive attributes for filtering/slicing
- Have unique key columns (one row per entity)
- Examples: Customer, Product, Date, Geography, Employee
- Naming convention: Singular noun (
Customer,Product)
Fact Tables
- Contain measurable, quantitative data
- Have foreign keys to dimensions
- Store data at consistent grain (one row per transaction/event)
- Examples: Sales, Orders, Inventory, WebVisits
- Naming convention: Business process noun (
Sales,Orders)
Design Principles
1. Separate Dimensions from Facts
BAD: Single denormalized "Sales" table with customer details
GOOD: "Sales" fact table + "Customer" dimension table2. Consistent Grain
Every row in a fact table represents the same thing:
- Order line level (most common)
- Daily aggregation
- Monthly summary
Never mix grains in one table.
3. Surrogate Keys
Add surrogate keys when source lacks unique identifiers:
// Power Query: Add index column
= Table.AddIndexColumn(Source, "CustomerKey", 1, 1)4. Date Dimension
Always create a dedicated date table:
- Mark as date table in Power BI
- Include fiscal periods if needed
- Add relative date columns (IsCurrentMonth, IsPreviousYear)
Date =
ADDCOLUMNS(
CALENDAR(DATE(2020,1,1), DATE(2030,12,31)),
"Year", YEAR([Date]),
"Month", FORMAT([Date], "MMMM"),
"MonthNum", MONTH([Date]),
"Quarter", "Q" & FORMAT([Date], "Q"),
"WeekDay", FORMAT([Date], "dddd")
)Special Dimension Types
Role-Playing Dimensions
Same dimension used multiple times (e.g., Date for OrderDate, ShipDate):
- Option 1: Duplicate the table (OrderDate, ShipDate tables)
- Option 2: Use inactive relationships with USERELATIONSHIP in DAX
Slowly Changing Dimensions (Type 2)
Track historical changes with version columns:
- StartDate, EndDate columns
- IsCurrent flag
- Requires pre-processing in data warehouse
Junk Dimensions
Combine low-cardinality flags into one table:
OrderFlags dimension: IsRush, IsGift, IsOnlineDegenerate Dimensions
Keep transaction identifiers (OrderNumber, InvoiceID) in fact table.
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Wide denormalized tables | Poor performance, hard to maintain | Split into star schema |
| Snowflake (normalized dims) | Extra joins hurt performance | Flatten dimensions |
| Many-to-many without bridge | Ambiguous results | Add bridge/junction table |
| Mixed grain facts | Incorrect aggregations | Separate tables per grain |
Validation Checklist
- [ ] Each table is clearly dimension or fact
- [ ] Fact tables have foreign keys to all related dimensions
- [ ] Dimensions have unique key columns
- [ ] Date table exists and is marked
- [ ] No circular relationship paths
- [ ] Consistent naming conventions
"""
Power BI Model Audit Script
This script audits a Power BI semantic model for best practices violations
and provides recommendations for improvement.
Prerequisites: Power BI Modeling MCP Server must be running
Usage: python scripts/powerbi-model-audit.py
"""
from typing import Dict, List, Any
class ModelAuditor:
"""Audits Power BI semantic models against best practices."""
def __init__(self):
self.issues = []
self.warnings = []
self.passed = []
def audit_model(self, model_data: Dict[str, Any]) -> Dict[str, List[str]]:
"""
Perform comprehensive model audit.
Args:
model_data: Model data from Power BI MCP operations
Returns:
Dictionary with issues, warnings, and passed checks
"""
self._audit_tables(model_data.get('tables', []))
self._audit_relationships(model_data.get('relationships', []))
self._audit_measures(model_data.get('measures', []))
return {
'issues': self.issues,
'warnings': self.warnings,
'passed': self.passed
}
def _audit_tables(self, tables: List[Dict]):
"""Audit table structure and documentation."""
if not tables:
self.issues.append("No tables found in model")
return
dimension_count = 0
fact_count = 0
for table in tables:
# Check for table description
if not table.get('description'):
self.warnings.append(f"Table '{table['name']}' is missing description")
# Check if marked as dimension or fact
if table.get('isHidden'):
self.passed.append(f"Table '{table['name']}' is hidden (appropriate for technical tables)")
# Check for hidden technical keys
for column in table.get('columns', []):
if 'Key' in column['name'] and not column.get('isHidden'):
self.warnings.append(f"Technical key '{column['name']}' in '{table['name']}' should be hidden")
def _audit_relationships(self, relationships: List[Dict]):
"""Audit relationship configuration."""
if not relationships:
self.issues.append("No relationships found - model may not be properly connected")
return
bidirectional_count = 0
for rel in relationships:
# Check for bidirectional filters
if rel.get('crossFilteringBehavior') == 'BothDirections':
bidirectional_count += 1
self.warnings.append(
f"Relationship '{rel['fromTable']}.{rel['fromColumn']}' → "
f"'{rel['toTable']}.{rel['toColumn']}' uses bidirectional filtering"
)
# Check for many-to-many relationships
if rel.get('fromCardinality') == 'many' and rel.get('toCardinality') == 'many':
self.issues.append(
f"Many-to-many relationship between '{rel['fromTable']}' and '{rel['toTable']}' "
"should be avoided - use bridging table instead"
)
# Check for active relationships
if not rel.get('isActive'):
self.warnings.append(
f"Relationship '{rel['fromTable']}' → '{rel['toTable']}' is inactive"
)
if bidirectional_count > 2:
self.issues.append(
f"Found {bidirectional_count} bidirectional relationships. "
"Limit to 1-2 for performance."
)
def _audit_measures(self, measures: List[Dict]):
"""Audit measure definitions."""
if not measures:
self.warnings.append("No measures found in model")
return
for measure in measures:
# Check for measure description
if not measure.get('description'):
self.warnings.append(f"Measure '{measure['name']}' in '{measure['tableName']}' is missing description")
# Check for format string
if not measure.get('formatString'):
self.warnings.append(f"Measure '{measure['name']}' has no format string")
# Check for common anti-patterns
expression = measure.get('expression', '')
if 'CALCULATE' not in expression and 'SUM' in expression and '*' in expression:
self.issues.append(
f"Measure '{measure['name']}' may have calculation issues - "
"use CALCULATE with proper filter context"
)
def print_audit_report(audit_results: Dict[str, List[str]]):
"""Print formatted audit report."""
print("=" * 80)
print("POWER BI MODEL AUDIT REPORT")
print("=" * 80)
print("\n✓ PASSED CHECKS:")
for item in audit_results['passed']:
print(f" ✓ {item}")
if not audit_results['passed']:
print(" None")
print("\n⚠ WARNINGS:")
for item in audit_results['warnings']:
print(f" ⚠ {item}")
if not audit_results['warnings']:
print(" None")
print("\n✗ ISSUES:")
for item in audit_results['issues']:
print(f" ✗ {item}")
if not audit_results['issues']:
print(" None")
print("\n" + "=" * 80)
print(f"Total: {len(audit_results['passed'])} passed, {len(audit_results['warnings'])} warnings, {len(audit_results['issues'])} issues")
print("=" * 80)
# MCP Integration Example
def audit_via_mcp():
"""
Example of using this with Power BI MCP tools.
In actual usage, you would call MCP operations to get model data:
- model_operations(operation="Get")
- table_operations(operation="List")
- measure_operations(operation="List")
- relationship_operations(operation="List")
"""
auditor = ModelAuditor()
# Mock data for demonstration
mock_model_data = {
'tables': [
{
'name': 'Sales',
'description': 'Sales transactions',
'columns': [
{'name': 'SalesKey', 'isHidden': True},
{'name': 'Amount', 'isHidden': False}
]
}
],
'relationships': [
{
'fromTable': 'Customer',
'toTable': 'Sales',
'crossFilteringBehavior': 'OneDirection',
'isActive': True
}
],
'measures': [
{
'name': 'Total Sales',
'tableName': 'Sales',
'expression': 'SUM(Sales[Amount])',
'formatString': '$#,##0',
'description': 'Total sales amount'
}
]
}
results = auditor.audit_model(mock_model_data)
print_audit_report(results)
if __name__ == '__main__':
audit_via_mcp()
Related skills
FAQ
What does powerbi-modeling do?
powerbi-modeling is a Claude Code skill for ai & agent building.
When should I use powerbi-modeling?
When you need to helps with ai & agent building tasks., or when powerbi-modeling is a claude code skill for ai & agent building.
What are the main capabilities?
powerbi-modeling; AI & Agent Building; AI-coding skill.