
Power Bi
- 45 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Helps with ai & agent building tasks.
About
power-bi is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- power-bi
- AI & Agent Building
- AI-coding skill
Power Bi by the numbers
- 45 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #7,734 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill power-biAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 45 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Power BI Development Skill
End-to-end Power BI project development using the PBIP (Power BI Project) format — the git-friendly, text-based format for version-controlled Power BI solutions.
When This Skill Applies
- Creating or modifying Power BI semantic models (TMDL files)
- Writing or debugging Power Query (M) expressions (.pq files)
- Authoring DAX measures
- Designing star schema data models
- Publishing reports to Power BI Service
- Configuring scheduled refresh and data gateways
- Troubleshooting PBI connectors (especially Azure Cost Management / EA)
- Working with any .tmdl, .pq, .pbip, .pbir, .pbism files
PBIP Project Structure
ProjectName/
├── ProjectName.pbip # Open this in PBI Desktop
├── ProjectName.Report/
│ ├── report.json # Report visuals (edit in PBI Desktop)
│ ├── definition.pbir # Report definition pointer
│ └── StaticResources/
│ └── SharedResources/BaseThemes/ # Custom themes (.json)
├── ProjectName.SemanticModel/
│ ├── definition.pbism # Semantic model pointer
│ └── definition/
│ ├── model.tmdl # Model-level settings
│ ├── tables/ # Table definitions + measures (.tmdl)
│ ├── expressions/ # Power Query scripts (.pq)
│ └── relationships.tmdl # Star schema joins
├── scripts/ # Python automation (optional)
└── docs/ # DocumentationWorkflow Routing
| Workflow | Trigger | File |
|---|---|---|
| NewProject | "create PBI project", "scaffold PBIP", "new Power BI project" | workflows/new-project.md |
| AddTable | "add table", "new fact table", "new dimension", "add PQ source" | workflows/add-table.md |
| AddMeasure | "add DAX measure", "create measure", "new KPI" | workflows/add-measure.md |
| DataModeling | "star schema", "add relationship", "data model design" | workflows/data-modeling.md |
| PublishRefresh | "publish to service", "scheduled refresh", "data gateway" | workflows/publish-refresh.md |
| ConnectorAuth | "connector auth", "EA connector", "cost management connector", "PBI sign in failed" | workflows/connector-auth.md |
| Troubleshooting | "PBI error", "refresh failed", "data not loading", "type error" | workflows/troubleshooting.md |
Reference Files
Read these as needed — don't load all at once:
| Reference | When to Read | File |
|---|---|---|
| TMDL Syntax | Writing or editing .tmdl files | references/tmdl-syntax.md |
| Power Query Patterns | Writing or editing .pq files | references/power-query-patterns.md |
| DAX Patterns | Writing DAX measures | references/dax-patterns.md |
| Star Schema Guide | Data model design decisions | references/star-schema-guide.md |
| PBI Service & Gateway | Publishing and refresh config | references/pbi-service-gateway.md |
Tools
File Operations (PBIP Development)
| Tool | Use For |
|---|---|
| Read | Read .tmdl, .pq, .pbip, .pbir, .pbism, report.json, theme.json files |
| Write | Create new .tmdl, .pq, .json files for new tables, expressions, themes |
| Edit | Modify existing .tmdl files (add measures, columns), edit .pq expressions, update relationships.tmdl |
| Glob | Find files by pattern: **/*.tmdl, **/*.pq, **/expressions/*.pq |
| Grep | Search across TMDL/PQ files: find measure names, lineageTags, column references |
Code Search & Navigation
| Tool | Use For |
|---|---|
Grep pattern: "lineageTag" | Verify lineageTag uniqueness across all .tmdl files |
Grep pattern: "displayFolder" | List all measure folders for organization |
Grep pattern: "USERELATIONSHIP" | Find measures using inactive relationships |
Grep pattern: "isActive: false" | Find inactive relationships that need USERELATIONSHIP |
Glob pattern: "**/*.pq" | List all Power Query expressions |
Glob pattern: "**/tables/*.tmdl" | List all table definitions |
Automation & Scripting
| Tool | Use For |
|---|---|
| Bash | Run Python scripts: python scripts/export_billing_data.py --period YYYYMM |
| Bash | Run anomaly detection: python scripts/detect_anomalies.py --data-folder ./data |
| Bash | Git operations on PBIP files (commit, diff, branch) |
| Bash | Install Python dependencies: pip install -r scripts/requirements.txt |
| Bash | Azure CLI for data export auth: az login, az account set |
Browser Automation (Power BI Service)
| Tool | Use For |
|---|---|
| Browser tools (mcp__claude-in-chrome__*) | Navigate Power BI Service web UI |
| navigate | Open Power BI workspaces, dataset settings, refresh history |
| form_input | Configure scheduled refresh, data source credentials, parameters |
| get_page_text / read_page | Read refresh history, error messages, dataset settings |
| javascript_tool | Interact with PBI Service UI elements |
| tabs_create_mcp | Open new tabs for PBI Service pages |
| gif_creator | Record multi-step PBI Service configuration for documentation |
Research & Documentation
| Tool | Use For |
|---|---|
| WebSearch | Look up DAX functions, TMDL syntax changes, PBI release notes |
| WebFetch | Fetch Microsoft Learn docs for PBI/DAX/M reference |
| microsoft_docs_search (MCP) | Search official Microsoft PBI documentation |
| microsoft_docs_fetch (MCP) | Fetch full PBI documentation pages |
| microsoft_code_sample_search (MCP) | Find DAX/M code samples from Microsoft docs |
| context7 (MCP) | Fetch current library docs for Azure SDK, PBI REST API |
Data Inspection
| Tool | Use For |
|---|---|
| Read | Inspect CSV billing exports (preview first rows) |
Bash wc -l | Count rows in large CSV files |
Bash head -5 | Preview CSV headers and first rows |
| Grep on CSV | Search for specific resource groups, subscriptions, or cost values |
Common Tool Sequences
Adding a new measure: 1. Grep for existing lineageTags → ensure uniqueness 2. Read _Measures.tmdl → understand patterns 3. Edit _Measures.tmdl → add the new measure
Adding a new table: 1. Write expressions/NewTable.pq → create PQ expression 2. Write tables/NewTable.tmdl → create table definition 3. Edit relationships.tmdl → add relationship 4. Grep for lineageTags → verify no conflicts
Troubleshooting connector auth: 1. Read workflows/connector-auth.md → get diagnosis steps 2. Browser tools → navigate to EA portal or PBI Service settings 3. Read project memory → check known EA enrollment details
Publishing and refresh: 1. Browser tools → navigate to PBI Service workspace 2. read_page → check current dataset settings 3. form_input → configure refresh schedule 4. get_page_text → verify refresh history
Critical Rules
1. PBIP format only — Never suggest .pbix for version-controlled projects. PBIP is the git-friendly format (TMDL = text, PQ = text, clean diffs).
2. Localization awareness — Ask the user what language their labels should be in. For Brazilian projects, use pt-BR labels, BRL currency format (R$ #,##0.00), and Portuguese month names.
3. Star schema discipline — All relationships must be M:1 from fact to dimension tables. Use crossFilteringBehavior: oneDirection. Use isActive: false + USERELATIONSHIP() in DAX for ambiguous paths (e.g., multiple date relationships).
4. Measures table pattern — All DAX measures go in a dedicated _Measures table (calculated table with ROW("MeasureColumn", 0)). Organize measures into displayFolder groups.
5. Power Query parameter — Use a Parameter_ExportFolder parameter for folder-based CSV ingestion. This makes the data source path configurable without editing PQ code.
6. lineageTag convention — Every table, measure, column, and relationship needs a unique lineageTag. Use descriptive kebab-case: m-custo-total, t-fact-usage, rel-usage-date.
7. Format strings — Currency: R$ #,##0.00 (or locale-appropriate). Percentage: 0.0%;-0.0%;0.0%. Integer: #,##0. Use the three-part format for percentages to handle negative values.
8. EA connector vs Azure RBAC — The PBI Cost Management connector for EA enrollments requires Enterprise Administrator (read-only) role at the billing account level. Standard Azure RBAC roles (Cost Management Reader, Billing Reader) do NOT work. See workflows/connector-auth.md.
Quick Examples
TMDL Measure
measure 'Custo Total' =
SUM(Fact_Usage[PretaxCost])
formatString: R$ #,##0.00
displayFolder: Custo
lineageTag: m-custo-totalTMDL Relationship
relationship Fact_Usage_to_Dim_Date
fromColumn: Fact_Usage.DateKey
toColumn: Dim_Date.Date
crossFilteringBehavior: oneDirectionPower Query — Folder-based CSV ingestion
let
Source = Folder.Files(Parameter_ExportFolder),
Filtered = Table.SelectRows(Source, each
Text.Contains([Name], "UsageDetails") and Text.EndsWith([Name], ".csv")),
Combined = Table.Combine(
Table.AddColumn(Filtered, "Data", each
Csv.Document([Content], [Delimiter=",", Encoding=65001])
)[Data]
)
in
CombinedDAX — Month-over-Month variance with safe division
measure 'Variacao MoM %' =
VAR CustoAtual = [Custo Mes Atual]
VAR CustoAnterior = [Custo Mes Anterior]
RETURN
IF(
CustoAnterior <> 0,
DIVIDE(CustoAtual - CustoAnterior, CustoAnterior),
BLANK()
)
formatString: 0.0%;-0.0%;0.0%
displayFolder: Variacao---
Gotchas
- PBIP vs PBIX one-way regeneration: Re-saving a
.pbipas.pbixand back can silently regenerate everylineageTagGUID and rewrite TMDL whitespace, producing a massive zero-semantic diff. Always round-trip through PBIP and review diffs before committing. - EA connector ignores Azure RBAC:
Cost Management ReaderandBilling Readerroles always fail for EA enrollments. Only Enterprise Administrator (read-only) at billing account level works. MCA enrollments use a completely different auth path. - `Parameter_ExportFolder` must be Text type, not a path literal: PQ accepts literals during dev but Service refresh evaluates parameters before credentials, throwing an opaque "formula.firewall" error that never names the offending parameter.
- Three-part format strings flip on zero:
0.0%;-0.0%;0.0%shows zero in positive form. Use0.0%;-0.0%;"-"if zero should render as a dash — positives look identical in review. - Folder.Files on a live export folder is non-deterministic: If exports overwrite the same filename mid-refresh, PQ may read a partial file with no error. Filter by
Date.From([Date modified])or partition by YYYYMM in the export path. - `crossFilteringBehavior: oneDirection` is the only correct spelling:
singleDirectionparses on import but is silently ignored. Misspelling makes a star schema go bidirectional, killing performance and creating ambiguous paths.
{
"skill_name": "power-bi",
"evals": [
{
"id": 1,
"prompt": "I need to add a new DAX measure to our HyperaFinOps PBIP project that calculates the cost per resource group as a percentage of total cost. It should go in the Custo display folder, use BRL formatting, and follow our existing patterns in _Measures.tmdl. The measure name should be 'Custo RG %' and show what percentage each resource group contributes to the total.",
"expected_output": "A TMDL measure definition using DIVIDE with ALL() to calculate percentage of total, with correct formatString (0.0%;-0.0%;0.0%), displayFolder (Custo), unique lineageTag (m-custo-rg-pct), and following the VAR/RETURN pattern",
"files": []
},
{
"id": 2,
"prompt": "We're getting a new CSV data source for Azure Advisor recommendations. The files are named like 'AdvisorRecommendations_53329720_YYYYMM.csv' and have columns: RecommendationId, Category, Impact, ImpactedResource, PotentialSavings, Description. I need to add this as a new dimension table called Dim_Advisor to our finops-powerbi PBIP project. Create the PQ expression and TMDL definition files, and add a relationship to Fact_Usage via the InstanceId/ImpactedResource columns.",
"expected_output": "Two files: expressions/Dim_Advisor.pq using Folder.Files pattern with filtering, type conversions, and null handling; tables/Dim_Advisor.tmdl with all columns, lineageTags, dataTypes; plus a relationship entry for relationships.tmdl",
"files": []
},
{
"id": 3,
"prompt": "My colleague Rogerio is trying to connect Power BI Desktop directly to Azure Cost Management for our EA enrollment 53329720 but keeps getting 'Access Denied'. He has Cost Management Reader role on the subscription. What's wrong and how do we fix it?",
"expected_output": "Explanation that EA connector requires Enterprise Administrator (read-only) at billing account level, not Azure RBAC. Step-by-step to grant the role via ea.azure.com, mention 30-min propagation delay, credential cache clearing, and the alternative export-based approach if EA Admin can't be granted.",
"files": []
}
]
}
measure 'Custo RG %' =
VAR CustoRG = [Custo Combinado]
VAR CustoTotal =
CALCULATE(
[Custo Combinado],
ALL(Dim_ResourceGroup)
)
RETURN
IF(
CustoTotal <> 0,
DIVIDE(CustoRG, CustoTotal),
BLANK()
)
formatString: 0.0%;-0.0%;0.0%
displayFolder: Custo
lineageTag: m-custo-rg-pct
{
"total_tokens": 41179,
"duration_ms": 59040,
"total_duration_seconds": 59.0
}
measure 'Custo RG %' =
VAR CustoRG = [Custo Total]
VAR CustoTotalGeral = CALCULATE([Custo Total], REMOVEFILTERS(Dim_ResourceGroup))
RETURN
IF(
CustoTotalGeral <> 0,
DIVIDE(CustoRG, CustoTotalGeral),
BLANK()
)
formatString: 0.0%;-0.0%;0.0%
displayFolder: Custo
lineageTag: m-custo-rg-pct
{
"total_tokens": 30555,
"duration_ms": 46797,
"total_duration_seconds": 46.8
}
let
Source = Folder.Files(Parameter_ExportFolder),
Filtered = Table.SelectRows(Source, each
Text.Contains([Name], "AdvisorRecommendations") and Text.EndsWith([Name], ".csv")),
Combined = Table.Combine(
Table.AddColumn(Filtered, "Data", each
let
Raw = Csv.Document([Content], [Delimiter=",", QuoteStyle=QuoteStyle.Csv, Encoding=65001]),
Headers = Table.PromoteHeaders(Raw, [PromoteAllScalars=true])
in
Headers
)[Data]
),
// Remove duplicates — keep latest recommendation per RecommendationId
Deduplicated = Table.Distinct(Combined, {"RecommendationId"}),
// Explicit type conversions
Typed = Table.TransformColumnTypes(Deduplicated, {
{"RecommendationId", type text},
{"Category", type text},
{"Impact", type text},
{"ImpactedResource", type text},
{"PotentialSavings", type number},
{"Description", type text}
}),
// Null handling — default empty strings and zero savings
CleanCategory = Table.TransformColumns(Typed, {
{"Category", each if _ = null or _ = "" then "(sem categoria)" else _, type text}
}),
CleanImpact = Table.TransformColumns(CleanCategory, {
{"Impact", each if _ = null or _ = "" then "(sem impacto)" else _, type text}
}),
CleanResource = Table.TransformColumns(CleanImpact, {
{"ImpactedResource", each if _ = null or _ = "" then "(sem recurso)" else _, type text}
}),
CleanSavings = Table.TransformColumns(CleanResource, {
{"PotentialSavings", each if _ = null then 0 else _, type number}
}),
CleanDescription = Table.TransformColumns(CleanSavings, {
{"Description", each if _ = null or _ = "" then "(sem descricao)" else _, type text}
})
in
CleanDescription
table Dim_Advisor
lineageTag: t-dim-advisor
column RecommendationId
dataType: string
lineageTag: col-advisor-recid
summarizeBy: none
sourceColumn: RecommendationId
isKey: true
column Category
dataType: string
lineageTag: col-advisor-category
summarizeBy: none
sourceColumn: Category
column Impact
dataType: string
lineageTag: col-advisor-impact
summarizeBy: none
sourceColumn: Impact
column ImpactedResource
dataType: string
lineageTag: col-advisor-impacted-resource
summarizeBy: none
sourceColumn: ImpactedResource
column PotentialSavings
dataType: double
formatString: R$ #,##0.00
lineageTag: col-advisor-potential-savings
summarizeBy: sum
sourceColumn: PotentialSavings
column Description
dataType: string
lineageTag: col-advisor-description
summarizeBy: none
sourceColumn: Description
partition Dim_Advisor = m
mode: import
source =
let Source = Expression.Evaluate(#"Dim_Advisor", #shared) in Source
// Append this to relationships.tmdl
// Relationship: Fact_Usage -> Dim_Advisor via InstanceId/ImpactedResource
// Many-to-One: multiple usage rows can map to one advisor recommendation resource
relationship Fact_Usage_to_Dim_Advisor
fromColumn: Fact_Usage.InstanceId
toColumn: Dim_Advisor.ImpactedResource
crossFilteringBehavior: oneDirection
{
"total_tokens": 43009,
"duration_ms": 88932,
"total_duration_seconds": 88.9
}
// Azure Advisor Recommendations dimension
// CSV source: AdvisorRecommendations_53329720_YYYYMM.csv
let
FolderPath = Parameter_ExportFolder,
Source = Folder.Files(FolderPath),
// Advisor exports follow pattern: AdvisorRecommendations_53329720_YYYYMM.csv
FilteredFiles = Table.SelectRows(Source, each
Text.Contains([Name], "AdvisorRecommendations") and
Text.EndsWith([Name], ".csv")),
ProcessFile = (fileContent as binary) =>
let
RawCSV = Csv.Document(fileContent, [Delimiter=",", QuoteStyle=QuoteStyle.Csv, Encoding=65001]),
PromotedHeaders = Table.PromoteHeaders(RawCSV, [PromoteAllScalars=true])
in
PromotedHeaders,
CombinedCSVs = Table.Combine(
Table.AddColumn(FilteredFiles, "Data", each ProcessFile([Content]))[Data]
),
// Type conversions
TypedTable = Table.TransformColumnTypes(CombinedCSVs, {
{"RecommendationId", type text},
{"Category", type text},
{"Impact", type text},
{"ImpactedResource", type text},
{"PotentialSavings", type number},
{"Description", type text}
}),
// Deduplicate by RecommendationId (keep latest across monthly files)
Distinct = Table.Distinct(TypedTable, {"RecommendationId"})
in
Distinct
table Dim_Advisor
lineageTag: dim-advisor-001
column RecommendationId
dataType: string
isKey: true
isHidden: true
lineageTag: dim-advisor-recid
summarizeBy: none
sourceColumn: RecommendationId
column Category
dataType: string
lineageTag: dim-advisor-category
summarizeBy: none
sourceColumn: Category
column Impact
dataType: string
lineageTag: dim-advisor-impact
summarizeBy: none
sourceColumn: Impact
column ImpactedResource
dataType: string
lineageTag: dim-advisor-resource
summarizeBy: none
sourceColumn: ImpactedResource
column PotentialSavings
dataType: double
formatString: R$ #,##0.00
lineageTag: dim-advisor-savings
summarizeBy: sum
sourceColumn: PotentialSavings
column Description
dataType: string
lineageTag: dim-advisor-desc
summarizeBy: none
sourceColumn: Description
partition Dim_Advisor = m
mode: import
source =
let Source = Dim_Advisor in Source
// Addition to relationships.tmdl - Advisor to Fact_Usage via InstanceId/ImpactedResource
relationship Fact_Usage_to_Dim_Advisor
fromColumn: Fact_Usage.InstanceId
toColumn: Dim_Advisor.ImpactedResource
crossFilteringBehavior: oneDirection
{
"total_tokens": 37502,
"duration_ms": 92584,
"total_duration_seconds": 92.6
}
Diagnosis: Power BI Cost Management Connector — Access Denied
Root Cause
Rogerio's Cost Management Reader (Azure RBAC) role does NOT work for the EA connector. The EA connector authenticates against the EA Billing layer, which is a completely separate permission system from Azure RBAC.
Required Role
Enterprise Administrator (read-only) at the EA billing account level, granted through ea.azure.com.
Fix Steps
1. An existing EA Admin signs into https://ea.azure.com 2. Navigate to Manage > Enrollment tab 3. Click + Add Administrator 4. Enter Rogerio's email 5. Select Read Only checkbox 6. Click Add 7. Wait up to 30 minutes for propagation 8. In Power BI Desktop: File > Options > Data Source Settings > Clear Permissions 9. Retry the connection
Alternative
If EA Admin role cannot be granted, use Cost Management Exports to Azure Blob Storage + Blob connector in PBI. This only requires Cost Management Contributor (Azure RBAC) + storage access.
Why RBAC Doesn't Work
| Role | Works for EA Connector? |
|---|---|
| Enterprise Administrator (read-only) | YES |
| Cost Management Reader (Azure RBAC) | NO |
| Billing Reader (Azure RBAC) | NO |
| Department Administrator | NO |
| Account Owner | NO |
{
"total_tokens": 31487,
"duration_ms": 63399,
"total_duration_seconds": 63.4
}
Diagnosis: Power BI Cost Management Connector — Access Denied
Root Cause
The Power BI Cost Management connector for EA enrollments does NOT use Azure RBAC roles. When Rogerio enters enrollment number 53329720, the connector authenticates against the EA Billing API, which has its own separate permission system managed through the EA Portal (ea.azure.com). His "Cost Management Reader" role on the subscription is completely irrelevant to this authentication path.
Fix
Rogerio needs an Enterprise Administrator (Read-Only) role assigned at the EA enrollment level in the EA Portal. An existing Enterprise Admin must grant this. After assignment, wait ~30 minutes for propagation, then retry.
{
"total_tokens": 25660,
"duration_ms": 52767,
"total_duration_seconds": 52.8
}
DAX Patterns Reference
Common DAX measure patterns for Power BI, with examples from an Azure FinOps project (BRL, pt-BR).
Measures Table Pattern
All measures in a dedicated _Measures calculated table:
table _Measures
lineageTag: measures-001
measure 'Custo Total' = SUM(Fact_Usage[PretaxCost])
formatString: R$ #,##0.00
displayFolder: Custo
lineageTag: m-custo-total
column MeasureColumn
dataType: int64
isHidden: true
lineageTag: measures-col
summarizeBy: none
sourceColumn: none
partition _Measures = calculated
source = ROW("MeasureColumn", 0)Core Aggregation
Simple SUM
measure 'Custo Total' = SUM(Fact_Usage[PretaxCost])CALCULATE with USERELATIONSHIP
For inactive relationships (multiple fact tables sharing a dimension):
measure 'Custo Marketplace' =
CALCULATE(
SUM(Fact_Marketplace[PretaxCost]),
USERELATIONSHIP(Fact_Marketplace[DateKey], Dim_Date[Date])
)Combining Measures
measure 'Custo Combinado' = [Custo Total] + [Custo Marketplace]Time Intelligence
Month-to-Date
measure 'Custo Mes Atual' =
CALCULATE([Custo Combinado], DATESMTD(Dim_Date[Date]))Previous Month
measure 'Custo Mes Anterior' =
CALCULATE([Custo Combinado], PREVIOUSMONTH(Dim_Date[Date]))Same Period Last Year
measure 'Custo Mesmo Mes Ano Anterior' =
CALCULATE([Custo Combinado], SAMEPERIODLASTYEAR(Dim_Date[Date]))Year-to-Date
measure 'Custo YTD' =
CALCULATE([Custo Combinado], DATESYTD(Dim_Date[Date]))YTD vs Prior Year YTD
measure 'Custo YTD Ano Anterior' =
CALCULATE([Custo YTD], SAMEPERIODLASTYEAR(Dim_Date[Date]))Safe Division & Variance
Always use VAR/RETURN with DIVIDE or IF for safe division:
Month-over-Month %
measure 'Variacao MoM %' =
VAR CustoAtual = [Custo Mes Atual]
VAR CustoAnterior = [Custo Mes Anterior]
RETURN
IF(
CustoAnterior <> 0,
DIVIDE(CustoAtual - CustoAnterior, CustoAnterior),
BLANK()
)Year-over-Year %
measure 'Variacao YoY %' =
VAR CustoAtual = [Custo Combinado]
VAR CustoAnoAnterior = [Custo Mesmo Mes Ano Anterior]
RETURN
IF(
CustoAnoAnterior <> 0,
DIVIDE(CustoAtual - CustoAnoAnterior, CustoAnoAnterior),
BLANK()
)Budget Utilization %
measure 'Utilizacao Orcamento %' =
IF([Orcamento] <> 0, DIVIDE([Custo Combinado], [Orcamento]), BLANK())Rolling Statistics
3-Month Moving Average
measure 'Media Movel 3M' =
AVERAGEX(
DATESINPERIOD(Dim_Date[Date], MAX(Dim_Date[Date]), -3, MONTH),
CALCULATE([Custo Combinado])
)Standard Deviation (for anomaly detection)
VAR Dates3M = DATESINPERIOD(Dim_Date[Date], MAX(Dim_Date[Date]), -3, MONTH)
VAR StdDev = STDEVX.P(Dates3M, CALCULATE([Custo Combinado]))Anomaly Detection
Flag costs that deviate significantly from the rolling average:
Anomaly Flag (>2 std dev)
measure 'Eh Anomalia' =
VAR CurrentCost = [Custo Combinado]
VAR AvgCost = [Media Movel 3M]
VAR Dates3M = DATESINPERIOD(Dim_Date[Date], MAX(Dim_Date[Date]), -3, MONTH)
VAR StdDev = STDEVX.P(Dates3M, CALCULATE([Custo Combinado]))
RETURN
IF(AND(StdDev > 0, ABS(CurrentCost - AvgCost) > 2 * StdDev), 1, 0)Severe Anomaly (>3 std dev)
Same pattern but > 3 * StdDev.
Anomaly Impact
measure 'Impacto Anomalia' =
IF([Eh Anomalia] = 1, [Custo Combinado] - [Media Movel 3M], BLANK())Budget Measures
Budget with USERELATIONSHIP
measure 'Orcamento' =
CALCULATE(
SUM(Dim_Budget[BudgetAmount]),
USERELATIONSHIP(Dim_Budget[BillingDate], Dim_Date[Date])
)Budget Variance
measure 'Variancia Orcamento' = [Orcamento] - [Custo Combinado]Tag Compliance
Percentage of compliant resources
measure 'Recursos Tagueados %' =
DIVIDE(
COUNTROWS(FILTER(Dim_ResourceGroup, Dim_ResourceGroup[TagsCompletas] = TRUE())),
COUNTROWS(Dim_ResourceGroup)
)Cost of non-compliant resources
measure 'Custo Sem Tag' =
CALCULATE([Custo Total], FILTER(Dim_ResourceGroup, Dim_ResourceGroup[TagsCompletas] = FALSE()))Per-tag compliance
measure 'Conformidade Executivo %' =
DIVIDE(
COUNTROWS(FILTER(Dim_ResourceGroup, Dim_ResourceGroup[TemTagExecutivo] = TRUE())),
COUNTROWS(Dim_ResourceGroup)
)Projection / Forecasting
Daily Average (current month)
measure 'Media Diaria Mes' =
VAR TotalCost = [Custo Mes Atual]
VAR DaysElapsed = COUNTROWS(
FILTER(DATESMTD(Dim_Date[Date]), Dim_Date[Date] <= TODAY())
)
RETURN DIVIDE(TotalCost, DaysElapsed)End-of-Month Projection
measure 'Projecao Fim Mes' =
VAR DailyAvg = [Media Diaria Mes]
VAR DaysInMonth = DAY(EOMONTH(MAX(Dim_Date[Date]), 0))
RETURN DailyAvg * DaysInMonthConditional Formatting Helpers
Return hex colors for visual formatting:
measure 'Cor Variacao' =
IF([Variacao MoM %] > 0.05, "#E74C3C",
IF([Variacao MoM %] < -0.05, "#27AE60",
"#F39C12"))- Red (#E74C3C): Cost increased >5%
- Green (#27AE60): Cost decreased >5%
- Yellow (#F39C12): Stable (within 5%)
Balance Measures (EA Commitment)
measure 'Saldo Compromisso EA' =
CALCULATE(
MAX(Fact_Balance[EndingBalance]),
USERELATIONSHIP(Fact_Balance[BillingDate], Dim_Date[Date]),
LASTDATE(Dim_Date[Date])
)Placeholder Measures
For features not yet implemented:
measure 'Cobertura RI %' =
BLANK()
formatString: 0.0%
displayFolder: Reservas
lineageTag: m-cobertura-ri
annotation note = "Placeholder - requires Reservation Details export data"Format String Reference
| Type | Format | Output Example |
|---|---|---|
| BRL | R$ #,##0.00 | R$ 1.458.793,00 |
| Percentage | 0.0%;-0.0%;0.0% | 12.5% / -3.2% / 0.0% |
| Integer | #,##0 | 1,234 |
| Count | 0 | 42 |
| Decimal | #,##0.00 | 1,234.56 |
Three-part percentage format: positive;negative;zero — handles all cases.
Power BI Service & Gateway Reference
Publishing from Desktop
1. Open .pbip in Power BI Desktop 2. Home > Publish 3. Select workspace (or create new) 4. If replacing existing: confirm overwrite 5. Dataset and report are published as separate items
After publishing, the dataset (semantic model) and report are independent — you can update one without the other.
On-Premises Data Gateway
When Needed
- Data source is local files (CSV folder, Excel)
- Data source is on-premises database (SQL Server, Oracle)
- Data source is on a network share
- Data source requires VPN or private network
Not Needed
- Cloud data sources (Azure SQL, Azure Blob Storage, Dataverse)
- Azure Cost Management connector
- SharePoint Online / OneDrive
Installation
1. Download: https://powerbi.microsoft.com/gateway/ 2. Install on a server (not a laptop):
- Always-on, reliable internet
- Access to all data sources
- .NET Framework 4.7.2+
3. Sign in with Power BI account 4. Register gateway name in tenant
Gateway Modes
| Mode | Use Case |
|---|---|
| Standard | Shared across users, managed centrally |
| Personal | Single user only, lighter weight |
For enterprise: always Standard.
Adding Data Sources
1. Power BI Service > Settings > Manage gateways 2. Select gateway > Add data source 3. Configure source type (Folder, SQL Server, etc.) 4. Set credentials 5. Test connection
Scheduled Refresh
Setup
1. Power BI Service > Workspace > Dataset > Settings 2. Gateway connection: map each source to gateway data source 3. Scheduled refresh: toggle ON 4. Set frequency: Daily at 08:00 (typical for FinOps) 5. Set timezone 6. Enable failure notifications 7. Apply
Refresh Limits
| License | Max Refreshes/Day |
|---|---|
| Pro | 8 |
| Premium Per User | 48 |
| Premium Capacity | 48 (configurable higher) |
Refresh History
Dataset > Refresh history — shows last N refreshes with:
- Start time, duration
- Status (success/failure)
- Error details for failures
Power BI Template Apps
Template apps are pre-built PBI solutions that connect to specific services.
Azure Cost Management Template App
1. Power BI Service > Apps > Get apps 2. Search "Azure Cost Management" 3. Install > Connect 4. Enter parameters:
- Scope: Enrollment Number
- Enrollment ID:
53329720 - Number of months: 13 (or desired range)
5. Sign in with organizational account (needs EA Admin role) 6. Wait for initial data load
Template App vs Custom PBIP
| Aspect | Template App | Custom PBIP |
|---|---|---|
| Setup time | Minutes | Days |
| Customization | Limited | Full control |
| Version control | No | Git-friendly |
| Data model | Fixed | Custom star schema |
| Measures | Pre-defined | Custom DAX |
Dataset Settings
Parameters
- View and update parameter values (e.g., Parameter_ExportFolder)
- Must match gateway data source path
Data Source Credentials
- OAuth2: organizational account (Azure AD)
- Key: account key or SAS token (storage)
- Basic: username/password (SQL)
Endorsement
- Promoted: recommended dataset
- Certified: verified by admin
Workspace Roles
| Role | Publish | Edit | View | Admin |
|---|---|---|---|---|
| Admin | Yes | Yes | Yes | Yes |
| Member | Yes | Yes | Yes | No |
| Contributor | Yes | Yes | Yes | No |
| Viewer | No | No | Yes | No |
Key differences:
- Admin: manage workspace settings, add/remove members
- Member: publish content, share items
- Contributor: publish content (cannot share)
- Viewer: consume only
REST API Quick Reference
Trigger Refresh
POST https://api.powerbi.com/v1.0/myorg/groups/{workspace-id}/datasets/{dataset-id}/refreshes
Authorization: Bearer {token}Get Dataset Info
GET https://api.powerbi.com/v1.0/myorg/groups/{workspace-id}/datasets/{dataset-id}Get Refresh History
GET https://api.powerbi.com/v1.0/myorg/groups/{workspace-id}/datasets/{dataset-id}/refreshesTroubleshooting
| Error | Cause | Fix |
|---|---|---|
| Gateway offline | Service stopped on host | Restart Windows service |
| Credential expired | OAuth token expired | Re-enter in dataset settings |
| File not found | Path mismatch Desktop vs Gateway | Match Parameter_ExportFolder to gateway source |
| Query timeout | Large dataset, slow PQ | Optimize queries, reduce scope |
| Access denied | Insufficient permissions | Check credentials have read access |
| "Data source not found" | Gateway source not configured | Add data source in gateway management |
Power Query (M) Patterns Reference
Common patterns for .pq files in PBIP semantic models.
1. Folder-Based CSV Ingestion
The core pattern for combining multiple billing period CSVs:
let
Source = Folder.Files(Parameter_ExportFolder),
Filtered = Table.SelectRows(Source, each
Text.Contains([Name], "UsageDetails") and Text.EndsWith([Name], ".csv")),
ProcessFile = (fileContent as binary) =>
let
RawCSV = Csv.Document(fileContent, [Delimiter=",", QuoteStyle=QuoteStyle.Csv, Encoding=65001]),
FirstCell = try RawCSV{0}{0} otherwise "",
SkipRows = if Text.Contains(FirstCell, "Usage") or Text.Contains(FirstCell, "Report") then 2 else 0,
Skipped = Table.Skip(RawCSV, SkipRows),
PromotedHeaders = Table.PromoteHeaders(Skipped, [PromoteAllScalars=true])
in
PromotedHeaders,
Combined = Table.Combine(
Table.AddColumn(Filtered, "Data", each ProcessFile([Content]))[Data]
)
in
CombinedKey points:
Folder.Files()discovers all files in the folderText.Contains+Text.EndsWithfilters by naming convention- Inner function handles per-file processing (header skipping, encoding)
Encoding=65001= UTF-8Table.Combinemerges all processed files
2. Column Renaming for Multiple CSV Versions
EA exports change column names across versions. Handle with List.Accumulate:
ColumnMapping = {
{"Date", "UsageDate"},
{"ExtendedCost", "PretaxCost"},
{"PreTaxCost", "PretaxCost"},
{"Cost", "PretaxCost"},
{"MeterRegion", "ResourceLocation"}
},
RenameExisting = List.Accumulate(
ColumnMapping,
PreviousStep,
(state, pair) =>
if List.Contains(Table.ColumnNames(state), pair{0}) and pair{0} <> pair{1}
then Table.RenameColumns(state, {{pair{0}, pair{1}}})
else state
)Only renames columns that actually exist — safe for mixed-version CSVs.
3. Type Conversions
Always explicit, never rely on auto-detection:
TypedTable = Table.TransformColumnTypes(RenameExisting, {
{"UsageDate", type date},
{"ConsumedQuantity", type number},
{"ResourceRate", type number},
{"PretaxCost", type number}
})Adding a DateKey Column
AddDateKey = Table.AddColumn(TypedTable, "DateKey", each [UsageDate], type date)4. Dimension Derivation from Fact Table
Extract unique dimension values from a fact table:
let
Source = Fact_Usage,
Distinct = Table.Distinct(
Table.SelectColumns(Source, {"SubscriptionGuid", "SubscriptionName", "AccountName", "DepartmentName"})
)
in
DistinctFor dimensions with classification logic:
let
Source = Fact_Usage,
Distinct = Table.Distinct(
Table.SelectColumns(Source, {"MeterCategory", "MeterSubCategory"})
),
AddTier = Table.AddColumn(Distinct, "ServiceTier", each
if Text.Contains([MeterCategory], "Virtual Machines") then "Compute"
else if Text.Contains([MeterCategory], "Storage") then "Storage"
else if Text.Contains([MeterCategory], "SQL") or Text.Contains([MeterCategory], "Cosmos") then "Database"
else "Outros",
type text)
in
AddTier5. JSON Tag Parsing
Parse Azure resource tags from a JSON column:
ParseTag = (tagsJson as nullable text, tagName as text) as nullable text =>
let
Result = if tagsJson = null or tagsJson = "" then null
else try Json.Document(tagsJson){[Key=tagName]}[Value]
otherwise
try Record.Field(Json.Document(tagsJson), tagName)
otherwise null
in
Result,
AddExecutivo = Table.AddColumn(Source, "Executivo", each
let tag = ParseTag([Tags], "Executivo")
in if tag = null then "(nao atribuido)" else tag, type text),
AddTagCompliance = Table.AddColumn(Previous, "TagsCompletas", each
[TemTagExecutivo] and [TemTagGestor] and [TemTagRotulo] and [TemTagAgrupamento] and [TemTagAmbiente],
type logical)The ParseTag function handles two JSON formats:
- Array of objects:
[{"Key":"Executivo","Value":"Allan Lima"}] - Simple record:
{"Executivo":"Allan Lima"}
6. Calendar Table Generation (pt-BR)
let
StartDate = #date(2024, 1, 1),
EndDate = #date(2027, 12, 31),
DayCount = Duration.Days(EndDate - StartDate) + 1,
DateList = List.Dates(StartDate, DayCount, #duration(1, 0, 0, 0)),
DateTable = Table.FromList(DateList, Splitter.SplitByNothing(), {"Date"}, null, ExtraValues.Error),
Typed = Table.TransformColumnTypes(DateTable, {{"Date", type date}}),
AddAno = Table.AddColumn(Typed, "Ano", each Date.Year([Date]), Int64.Type),
AddMes = Table.AddColumn(AddAno, "Mes", each Date.Month([Date]), Int64.Type),
MonthNames = {"Janeiro","Fevereiro","Marco","Abril","Maio","Junho",
"Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"},
AddNomeMes = Table.AddColumn(AddMes, "NomeMes", each MonthNames{[Mes]-1}, type text),
MonthAbbrev = {"Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"},
AddAbrev = Table.AddColumn(AddNomeMes, "NomeMesAbrev", each MonthAbbrev{[Mes]-1}, type text),
AddAnoMes = Table.AddColumn(AddAbrev, "AnoMes", each
Text.From([Ano]) & Text.PadStart(Text.From([Mes]), 2, "0"), type text),
AddLabel = Table.AddColumn(AddAnoMes, "AnoMesLabel", each
[NomeMesAbrev] & "/" & Text.From([Ano]), type text),
AddTrimestre = Table.AddColumn(AddLabel, "Trimestre", each
"T" & Text.From(Number.RoundUp([Mes] / 3)), type text),
DayNames = {"Segunda","Terca","Quarta","Quinta","Sexta","Sabado","Domingo"},
AddDiaSemana = Table.AddColumn(AddTrimestre, "DiaSemana", each
DayNames{Date.DayOfWeek([Date], Day.Monday)}, type text),
AddEhDiaUtil = Table.AddColumn(AddDiaSemana, "EhDiaUtil", each
Date.DayOfWeek([Date], Day.Monday) < 5, type logical)
in
AddEhDiaUtil7. Key-Value CSV Parsing (EA Balance)
EA Balance exports use a key-value format, not tabular:
let
Source = Folder.Files(Parameter_ExportFolder),
Filtered = Table.SelectRows(Source, each
Text.Contains([Name], "ArmBalances") and Text.EndsWith([Name], ".csv")),
ProcessBalance = (content as binary, fileName as text) =>
let
Lines = Lines.FromBinary(content, null, null, 65001),
// Extract billing period from filename: ArmBalances_53329720_YYYYMM_en.csv
Period = Text.BetweenDelimiters(fileName, "_", "_", 1),
// Parse key-value lines: "Key,Currency,Value"
ParseLine = (line as text) =>
let parts = Text.Split(line, ",")
in if List.Count(parts) >= 3 then {parts{0}, parts{2}} else null,
Parsed = List.Select(List.Transform(Lines, ParseLine), each _ <> null),
AsRecord = Record.FromList(
List.Transform(Parsed, each _{1}),
List.Transform(Parsed, each _{0})
)
in
Record.AddField(AsRecord, "BillingPeriod", Period)
in
...8. Parameter Usage
Create a configurable parameter in a .pq file:
"C:\data\billing-exports" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true]Reference in other queries:
let
FolderPath = Parameter_ExportFolder,
Source = Folder.Files(FolderPath),
...9. Null Handling Patterns
// Replace null ResourceGroup
CleanRG = Table.TransformColumns(Previous, {
{"ResourceGroup", each if _ = null or _ = "" then "(sem grupo)" else _, type text}
}),
// Default for missing tags
AddTag = Table.AddColumn(Previous, "Executivo", each
let tag = ParseTag([Tags], "Executivo")
in if tag = null then "(nao atribuido)" else tag, type text),
// Safe number conversion (null → 0)
SafeNumber = Table.TransformColumns(Previous, {
{"PretaxCost", each if _ = null then 0 else _, type number}
})Tips
- Always specify
Encoding=65001(UTF-8) for CSV parsing - Use
try ... otherwisefor resilient parsing - Filter files early (
Table.SelectRowson file names) before processing content - Test each step individually in Power Query Editor before combining
- Use
Table.Buffer()for tables referenced multiple times (performance)
Star Schema Guide for Power BI
Why Star Schema
Power BI's DAX engine is optimized for star schemas:
- Performance: Columnar storage + star schema = fast aggregations
- DAX simplicity: Time intelligence, CALCULATE filters, and USERELATIONSHIP all assume M:1 relationships
- Slicer behavior: One-direction cross-filtering from dimension to fact works naturally
- Maintainability: Clear separation of "what happened" (facts) vs "how to slice it" (dimensions)
Fact Table Design
Facts store measurements — things you count, sum, or average.
Grain Definition
Define the finest level of detail before designing:
- Fact_Usage: "One row per meter per resource per day"
- Fact_Marketplace: "One row per marketplace meter per day"
- Fact_Balance: "One row per billing period (monthly)"
Required Elements
- Foreign keys to each dimension (DateKey, SubscriptionGuid, ResourceGroup, etc.)
- Numeric columns for aggregation (PretaxCost, ConsumedQuantity)
- DateKey column for the Dim_Date relationship
Anti-patterns
- Don't store descriptive text in facts (put it in dimensions)
- Don't pre-aggregate (keep the finest grain)
- Don't duplicate dimension attributes in facts
Dimension Table Design
Dimensions store descriptive attributes for filtering and grouping.
Derivation Patterns
From fact table (most common):
Table.Distinct(Table.SelectColumns(Fact_Usage, {"SubscriptionGuid", "SubscriptionName"}))Generated (calendar):
// Generate date range with locale-specific attributes
List.Dates(#date(2024,1,1), DayCount, #duration(1,0,0,0))Manual (budget, targets):
#table(type table [Period=text, Amount=number], {{"202601", 1000000}})Enriched (tags, classification):
// Derive from fact, then add parsed tags and compliance flags
Table.Distinct(...) → AddTag → AddComplianceKey Decisions
- Use natural keys (SubscriptionGuid) when they're stable and unique
- Use surrogate keys (integer) when natural keys are long or composite
- Always have a "missing" row for orphaned fact records:
"(sem grupo)","(nao atribuido)"
Relationship Rules
┌──────────────┐
│ Dim_Date │
│ (Calendar) │
└──────┬───────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌───────┴────────┐ ┌──────┴────────┐ ┌──────┴──────────┐
│ Fact_Usage │ │Fact_Marketplace│ │ Fact_Balance │
│ (ACTIVE→Date) │ │ (INACTIVE) │ │ (INACTIVE) │
└───────┬────────┘ └──────┬────────┘ └─────────────────┘
│ │
┌────┴────┐ ┌───┴────┐
│ │ │ │
┌──┴─────┐┌──┴────┐┌──┴──────┐│
│Dim_Sub ││Dim_RG ││Dim_Pub ││
│ ││ ││lisher ││
└────────┘└───┬───┘└─────────┘│
│ │
┌────┴────┐ │
│Dim_Svc │ │
└─────────┘ ┌────┴─────┐
│Dim_Budget│
└──────────┘Rule 1: All M:1 from Fact to Dimension
// CORRECT
relationship Fact_Usage_to_Dim_Date
fromColumn: Fact_Usage.DateKey // Many side
toColumn: Dim_Date.Date // One side
crossFilteringBehavior: oneDirectionRule 2: One Active Relationship Per Path
When multiple fact tables share Dim_Date:
- Fact_Usage → Dim_Date: ACTIVE (most queried)
- Fact_Marketplace → Dim_Date: INACTIVE (
isActive: false) - Fact_Balance → Dim_Date: INACTIVE
- Dim_Budget → Dim_Date: INACTIVE
Rule 3: USERELATIONSHIP for Inactive
Every measure touching an inactive relationship must wrap in CALCULATE:
measure 'Custo Marketplace' =
CALCULATE(
SUM(Fact_Marketplace[PretaxCost]),
USERELATIONSHIP(Fact_Marketplace[DateKey], Dim_Date[Date])
)Rule 4: One-Direction Cross-Filtering
Always crossFilteringBehavior: oneDirection. Bidirectional cross-filtering:
- Causes ambiguous filter propagation
- Can produce incorrect totals
- Breaks DAX expectations
Multiple Fact Tables
When facts share dimensions:
1. Create the dimension once (derive from the primary fact) 2. Add FK columns to all fact tables that reference it 3. Make the primary fact's relationship active, others inactive 4. Write USERELATIONSHIP measures for inactive paths
Shared dimensions in a FinOps model:
- Dim_Date: shared by Fact_Usage (active), Fact_Marketplace, Fact_Balance, Dim_Budget (all inactive)
- Dim_Subscription: shared by Fact_Usage (active), Fact_Marketplace (inactive)
- Dim_ResourceGroup: shared by Fact_Usage (active), Fact_Marketplace (inactive)
Calendar Table (Dim_Date)
Every model needs one. Requirements:
- Continuous dates (no gaps) covering the full data range
- isKey: true on the Date column
- Sort columns: AnoMes (YYYYMM) for proper month ordering
- Localized: month names, day names, quarter labels in the project language
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Snowflaking | Dim → Dim chains break DAX filter context | Flatten into single dimension |
| Bidirectional cross-filter | Ambiguous paths, wrong totals | Always oneDirection |
| Many-to-Many without bridge | Duplicated rows in aggregation | Add bridge table or redesign |
| Calculated columns for measures | Wastes memory, can't be filtered | Use DAX measures instead |
| Multiple active paths to same dim | TMDL error / ambiguous | Only one active per path |
| Dates in fact without calendar | No time intelligence | Always add Dim_Date |
| Pre-aggregated facts | Lose detail, can't drill down | Keep finest grain |
TMDL Syntax Reference
Tabular Model Definition Language — the text-based format for PBIP semantic models.
File Structure
definition/
├── model.tmdl # Model-level settings (culture, version)
├── tables/ # One .tmdl per table
│ ├── _Measures.tmdl # Dedicated measures table
│ ├── Fact_Usage.tmdl # Fact table definitions
│ ├── Dim_Date.tmdl # Dimension table definitions
│ └── ...
├── expressions/ # One .pq per Power Query expression
│ ├── Fact_Usage.pq
│ ├── Dim_Date.pq
│ └── ...
└── relationships.tmdl # All relationships in one fileModel Definition (model.tmdl)
model Model
culture: pt-BR
defaultPowerBIDataSourceVersion: powerBI_V3
sourceQueryCulture: pt-BRculture: Locale for formatting (pt-BR, en-US, etc.)defaultPowerBIDataSourceVersion: AlwayspowerBI_V3for modern modelssourceQueryCulture: Affects how PQ interprets literals
Table Definition
table Fact_Usage
lineageTag: t-fact-usage
column UsageDate
dataType: dateTime
lineageTag: col-usage-date
summarizeBy: none
sourceColumn: UsageDate
column PretaxCost
dataType: double
lineageTag: col-pretax-cost
summarizeBy: sum
sourceColumn: PretaxCost
formatString: R$ #,##0.00
column ResourceGroup
dataType: string
lineageTag: col-rg
summarizeBy: none
sourceColumn: ResourceGroup
column TagsCompletas
dataType: boolean
lineageTag: col-tags-completas
summarizeBy: none
sourceColumn: TagsCompletas
partition Fact_Usage = m
mode: import
source =
let Source = Expression.Evaluate(#"Fact_Usage", #shared) in SourceColumn Properties
| Property | Required | Description |
|---|---|---|
dataType | Yes | string, int64, double, dateTime, boolean, decimal |
lineageTag | Yes | Unique identifier across model |
summarizeBy | Yes | none, sum, count, min, max, average |
sourceColumn | Yes | Column name from PQ output |
formatString | No | Display format (currency, percentage, etc.) |
isHidden | No | Hide from report view (true/false) |
isKey | No | Mark as primary key (true/false) |
sortByColumn | No | Column to sort by (e.g., sort month name by month number) |
annotation | No | Metadata annotations (key = value) |
dataType Values
| dataType | Use For | PQ Equivalent |
|---|---|---|
string | Text, IDs, names | type text |
int64 | Integers, counts | Int64.Type |
double | Decimals, amounts | type number |
dateTime | Dates and timestamps | type date, type datetime |
boolean | True/false flags | type logical |
decimal | Precise currency | Currency.Type |
Measure Definition
measure 'Custo Total' =
SUM(Fact_Usage[PretaxCost])
formatString: R$ #,##0.00
displayFolder: Custo
lineageTag: m-custo-totalMulti-line DAX Measure
measure 'Variacao MoM %' =
VAR CustoAtual = [Custo Mes Atual]
VAR CustoAnterior = [Custo Mes Anterior]
RETURN
IF(
CustoAnterior <> 0,
DIVIDE(CustoAtual - CustoAnterior, CustoAnterior),
BLANK()
)
formatString: 0.0%;-0.0%;0.0%
displayFolder: Variacao
lineageTag: m-variacao-momMeasure with Annotation
measure 'Contagem Reservas' =
BLANK()
formatString: 0
displayFolder: Reservas
lineageTag: m-contagem-reservas
annotation note = "Placeholder - requires Reservation Details export data"Measure Properties
| Property | Required | Description |
|---|---|---|
formatString | Recommended | Display format |
displayFolder | Recommended | Organize in field list |
lineageTag | Yes | Unique ID with m- prefix |
annotation | No | Metadata (key = "value") |
Calculated Table Pattern (_Measures)
The dedicated measures table pattern:
table _Measures
lineageTag: measures-001
// All measures go here organized by displayFolder sections
measure 'Custo Total' =
SUM(Fact_Usage[PretaxCost])
formatString: R$ #,##0.00
displayFolder: Custo
lineageTag: m-custo-total
// Hidden column required for calculated table
column MeasureColumn
dataType: int64
isHidden: true
lineageTag: measures-col
summarizeBy: none
sourceColumn: none
partition _Measures = calculated
source = ROW("MeasureColumn", 0)Key points:
partition = calculatedwithsource = ROW(...)makes it a calculated table- Hidden column exists only to satisfy the table requirement
sourceColumn: nonefor calculated columns
Relationship Definition
relationship Fact_Usage_to_Dim_Date
fromColumn: Fact_Usage.DateKey
toColumn: Dim_Date.Date
crossFilteringBehavior: oneDirectionInactive Relationship
relationship Fact_Marketplace_to_Dim_Date
fromColumn: Fact_Marketplace.DateKey
toColumn: Dim_Date.Date
isActive: false
crossFilteringBehavior: oneDirectionRelationship Properties
| Property | Required | Values |
|---|---|---|
fromColumn | Yes | TableName.ColumnName (many side) |
toColumn | Yes | TableName.ColumnName (one side) |
crossFilteringBehavior | Yes | oneDirection (always for star schema) |
isActive | No | false for inactive (default: true) |
Partition Types
Import from PQ Expression
partition Fact_Usage = m
mode: import
source =
let Source = Expression.Evaluate(#"Fact_Usage", #shared) in SourceCalculated Table
partition _Measures = calculated
source = ROW("MeasureColumn", 0)lineageTag Convention
| Object Type | Prefix | Example |
|---|---|---|
| Table | t- | t-fact-usage |
| Measure | m- | m-custo-total |
| Column | col- | col-pretax-cost |
| Relationship | rel- | rel-usage-date |
| Special | descriptive | measures-001, measures-col |
Rules:
- Kebab-case (lowercase with hyphens)
- Must be unique across the entire model
- Descriptive but concise
- Portuguese abbreviations OK:
m-conf-executivo,m-variacao-mom
Format Strings
| Type | Format | Example |
|---|---|---|
| BRL Currency | R$ #,##0.00 | R$ 1.458.793,00 |
| USD Currency | $ #,##0.00 | $1,458,793.00 |
| EUR Currency | € #,##0.00 | €1,458,793.00 |
| Percentage | 0.0%;-0.0%;0.0% | 12.5% / -3.2% / 0.0% |
| Integer | #,##0 | 1,234 |
| Decimal | #,##0.00 | 1,234.56 |
| Count | 0 | 42 |
| Date | dd/MM/yyyy | 15/04/2026 |
Workflow: Add DAX Measure
Add a new DAX measure to the _Measures table.
Step 1: Identify the Measure Category
Choose a displayFolder:
| Category | Folder Name | Typical Measures |
|---|---|---|
| Cost aggregation | Custo | SUM, CALCULATE with filters |
| Variance/comparison | Variacao | MoM %, YoY %, period comparisons |
| Budget | Orcamento | Budget vs actual, utilization % |
| Analytics | Analytics | Moving averages, projections, trends |
| Anomaly detection | Anomalias | Statistical outlier flags |
| Balance/commitment | Balance | EA balance, overage amounts |
| Compliance | Conformidade Tags | Tag coverage percentages |
| Reservation | Reservas | RI utilization, coverage, savings |
| Internal helpers | _Helpers | Conditional formatting, row counts |
Step 2: Write the Measure
Add to tables/_Measures.tmdl under the appropriate section.
Template
measure 'Measure Name' =
<DAX expression>
formatString: <format>
displayFolder: <folder>
lineageTag: m-measure-name-kebabCommon Patterns
Simple aggregation:
measure 'Total Cost' =
SUM(Fact_Usage[PretaxCost])
formatString: R$ #,##0.00
displayFolder: Custo
lineageTag: m-total-costWith USERELATIONSHIP (for inactive relationships):
measure 'Marketplace Cost' =
CALCULATE(
SUM(Fact_Marketplace[PretaxCost]),
USERELATIONSHIP(Fact_Marketplace[DateKey], Dim_Date[Date])
)
formatString: R$ #,##0.00
displayFolder: Custo
lineageTag: m-marketplace-costSafe percentage with VAR/RETURN:
measure 'Variance %' =
VAR Current = [Current Measure]
VAR Previous = [Previous Measure]
RETURN
IF(
Previous <> 0,
DIVIDE(Current - Previous, Previous),
BLANK()
)
formatString: 0.0%;-0.0%;0.0%
displayFolder: Variacao
lineageTag: m-variance-pctTime intelligence:
measure 'MTD Cost' =
CALCULATE([Combined Cost], DATESMTD(Dim_Date[Date]))
formatString: R$ #,##0.00
displayFolder: Custo
lineageTag: m-mtd-costCounting with filter:
measure 'Non-Compliant Count' =
COUNTROWS(FILTER(Dim_ResourceGroup, Dim_ResourceGroup[TagsCompletas] = FALSE()))
formatString: 0
displayFolder: Conformidade Tags
lineageTag: m-non-compliant-countPlaceholder (data not yet available):
measure 'RI Coverage %' =
BLANK()
formatString: 0.0%
displayFolder: Reservas
lineageTag: m-ri-coverage
annotation note = "Placeholder - requires Reservation Details export data"Step 3: Format String Reference
| Data Type | Format String | Example Output |
|---|---|---|
| BRL currency | R$ #,##0.00 | R$ 1.458.793,00 |
| USD currency | $ #,##0.00 | $ 1,458,793.00 |
| Percentage | 0.0%;-0.0%;0.0% | 12.5% / -3.2% / 0.0% |
| Integer | #,##0 | 1,234 |
| Count | 0 | 42 |
| Decimal | #,##0.00 | 1,234.56 |
The three-part percentage format handles positive, negative, and zero values.
Step 4: lineageTag Convention
- Prefix:
m-for all measures - Kebab-case:
m-custo-total,m-variacao-mom,m-conf-executivo - Must be unique across the entire model
- Keep it descriptive but concise
Checklist
- [ ] Measure added to _Measures.tmdl
- [ ] Correct formatString for the data type
- [ ] displayFolder assigned
- [ ] Unique lineageTag with m- prefix
- [ ] Uses DIVIDE() instead of / for safe division
- [ ] Uses USERELATIONSHIP() if accessing inactive relationship
- [ ] VAR/RETURN pattern for multi-step calculations
- [ ] BLANK() returned for edge cases (not 0 or error)
Workflow: Add Table
Add a new fact or dimension table to the PBIP semantic model.
Decision: Fact vs Dimension
| If the table... | It's a... |
|---|---|
| Contains transactional/event records | Fact table |
| Contains reference/lookup data | Dimension table |
| Has numeric columns for SUM/COUNT | Fact table |
| Has descriptive columns for filtering/grouping | Dimension table |
| Grows over time (new rows each period) | Fact table |
| Relatively static or slowly changing | Dimension table |
Step 1: Create the Power Query Expression
Create expressions/TableName.pq with the M expression.
For CSV-based fact tables — use Folder.Files pattern:
let
Source = Folder.Files(Parameter_ExportFolder),
Filtered = Table.SelectRows(Source, each
Text.Contains([Name], "FilePattern") and Text.EndsWith([Name], ".csv")),
Combined = Table.Combine(
Table.AddColumn(Filtered, "Data", each
let
Raw = Csv.Document([Content], [Delimiter=",", Encoding=65001]),
Headers = Table.PromoteHeaders(Raw, [PromoteAllScalars=true])
in Headers
)[Data]
),
Typed = Table.TransformColumnTypes(Combined, {
{"DateColumn", type date},
{"AmountColumn", type number}
}),
AddDateKey = Table.AddColumn(Typed, "DateKey", each [DateColumn], type date)
in
AddDateKeyFor derived dimension tables — extract from fact:
let
Source = FactTableName,
Distinct = Table.Distinct(
Table.SelectColumns(Source, {"KeyColumn", "DescColumn1", "DescColumn2"})
)
in
DistinctFor manually maintained dimensions (like Budget):
let
Source = #table(
type table [Period=text, Amount=number, Category=text],
{
{"202601", 1000000, "Total"},
{"202602", 1200000, "Total"}
}
)
in
SourceStep 2: Create the TMDL Definition
Create tables/TableName.tmdl:
table TableName
lineageTag: t-table-name
column ColumnName
dataType: string
lineageTag: col-column-name
summarizeBy: none
sourceColumn: ColumnName
column AmountColumn
dataType: double
lineageTag: col-amount
summarizeBy: sum
sourceColumn: AmountColumn
formatString: R$ #,##0.00
column DateKey
dataType: dateTime
lineageTag: col-datekey
summarizeBy: none
sourceColumn: DateKey
isKey: true
partition TableName = m
mode: import
source =
let
Source = Expression.Evaluate(
#"TableName",
#shared
)
in
SourceColumn dataType Reference
| Power Query Type | TMDL dataType | summarizeBy |
|---|---|---|
| type text | string | none |
| type number | double | sum (amounts) or none (rates) |
| type date | dateTime | none |
| Int64.Type | int64 | none or sum |
| type logical | boolean | none |
| Currency.Type | decimal | sum |
Step 3: Add Relationships
Append to relationships.tmdl:
relationship TableName_to_DimName
fromColumn: TableName.ForeignKey
toColumn: DimName.PrimaryKey
crossFilteringBehavior: oneDirectionIf the dimension already has an active relationship from another fact table, add isActive: false and use USERELATIONSHIP() in DAX measures.
Step 4: Verify
1. Open .pbip in Power BI Desktop 2. Refresh data 3. Check table appears in model view 4. Verify relationship lines in diagram 5. Test a simple measure against the new table
Checklist
- [ ] .pq file created in expressions/
- [ ] .tmdl file created in tables/ with all columns
- [ ] Every column has lineageTag, dataType, summarizeBy
- [ ] DateKey column added for date relationship
- [ ] Relationship added to relationships.tmdl
- [ ] Null handling in PQ (defaults for empty values)
- [ ] Type conversions explicit in PQ
Workflow: Connector Authentication
Troubleshoot and configure Power BI data source connectors, especially Azure Cost Management for EA enrollments.
Azure Cost Management Connector (EA)
The built-in Power BI connector for Azure Cost Management has different permission requirements depending on the billing account type.
EA Enrollment Connector
Required Role: Enterprise Administrator (read-only) at the billing account level.
This is the most common gotcha. Standard Azure RBAC roles do NOT work:
| Role | Works for EA Connector? |
|---|---|
| Enterprise Administrator (read-only) | YES |
| Enterprise Administrator (full) | YES |
| Cost Management Reader (Azure RBAC) | NO |
| Billing Reader (Azure RBAC) | NO |
| Department Administrator | NO |
| Account Owner | NO |
| Subscription Contributor | NO |
Why RBAC Doesn't Work
The EA connector uses OAuth2 against the EA Billing layer, which is a completely separate permission system from Azure RBAC. Granting Cost Management Reader at any scope does nothing for the EA connector.
Granting EA Admin (Read-Only)
1. Sign in to https://ea.azure.com as an existing EA Admin 2. Navigate to Manage > Enrollment tab 3. Click + Add Administrator 4. Enter the user's email (must be in the Entra ID tenant) 5. Select Read Only checkbox 6. Click Add 7. Wait up to 30 minutes for propagation
Connecting in Power BI Desktop
1. Get Data > Azure > Azure Cost Management 2. Choose scope: Enrollment Number 3. Enter enrollment ID (e.g., 53329720) 4. Click Connect 5. Sign in with the account that has EA Admin role 6. For guest accounts: use "Sign into an organization" flow and enter the host tenant FQDN
Common Failures
| Error | Cause | Fix |
|---|---|---|
| "Access denied" or 401 | Missing EA Admin role | Grant EA Admin (read-only) |
| "Sign-in failed" for guest | Cross-tenant guest account | Use "Sign into an organization" with host tenant domain |
| "No data returned" | EA policies blocking | Enable "DA view charges" and "AO view charges" in Billing Account > Policies |
| Stale token after role change | PBI credential cache | File > Options > Data Source Settings > Clear Permissions, then reconnect |
| Works in Desktop, fails in Service | Different account or missing consent | Re-enter credentials in dataset settings with EA Admin account |
EA Policy Prerequisites
Both of these must be enabled in the EA portal (Billing Account > Policies):
1. DA view charges — Department Administrators can see costs 2. AO view charges — Account Owners can see costs
If these are disabled, even EA Admins may see limited data.
Alternative: Export-Based Approach
If EA Admin role cannot be granted (common in large enterprises with strict governance):
1. Use Cost Management Exports to Azure Blob Storage
- Requires
Cost Management Contributor(Azure RBAC) + storage access - Configure in Azure Portal > Cost Management > Exports
2. In Power BI, use Azure Blob Storage connector instead
- Get Data > Azure > Azure Blob Storage
- Enter storage account URL and key/SAS token
3. Power Query reads the exported CSVs from blob
This avoids the EA billing layer entirely and uses standard Azure RBAC.
Other Connectors
Azure Blob Storage
- Needs: Storage account URL + account key or SAS token
- Or: Entra ID account with
Storage Blob Data Readerrole
SQL Database
- Needs: Server name, database name
- Auth: SQL auth (username/password) or Entra ID
- For Service: may need gateway if private endpoint
SharePoint / OneDrive
- Needs: Site URL
- Auth: Organizational account
- Common issue: URL must be the SharePoint site URL, not the file URL
Token Cache Management
When credentials change (role granted, password reset, etc.):
1. Power BI Desktop: File > Options > Data Source Settings > Clear Permissions 2. Power BI Service: Dataset Settings > Data source credentials > Edit credentials 3. Browser: Clear cookies for login.microsoftonline.com
Workflow: Data Modeling (Star Schema)
Design and implement a star schema data model in PBIP.
Step 1: Identify Fact Tables
Each fact table represents a business process or measurement:
| Question | Answer → Fact Table |
|---|---|
| What are we measuring? | Usage costs → Fact_Usage |
| What are the marketplace charges? | 3P costs → Fact_Marketplace |
| What is the EA commitment balance? | Monthly summary → Fact_Balance |
Grain: Define the finest level of detail. E.g., "one row per meter per resource per day."
Step 2: Identify Dimensions
For each fact table, identify the axes of analysis:
| Axis | Dimension | Key Column |
|---|---|---|
| When? | Dim_Date | Date/DateKey |
| Which subscription? | Dim_Subscription | SubscriptionGuid |
| Which resource group? | Dim_ResourceGroup | ResourceGroup |
| Which service? | Dim_Service | MeterCategory |
| Which publisher? | Dim_Publisher | PublisherName |
| What budget? | Dim_Budget | BillingDate |
Step 3: Define Relationships
All relationships must be Many-to-One (M:1) from fact to dimension.
Rules
1. One-direction cross-filtering — always crossFilteringBehavior: oneDirection 2. One active relationship per path — if two fact tables share Dim_Date, only one can be active. The other uses isActive: false. 3. USERELATIONSHIP in DAX — measures that need inactive relationships use CALCULATE(..., USERELATIONSHIP(Fact.FK, Dim.PK))
Active vs Inactive Decision
When multiple fact tables share a dimension:
Fact_Usage → Dim_Date (ACTIVE)
Fact_Marketplace → Dim_Date (INACTIVE — use USERELATIONSHIP)
Fact_Balance → Dim_Date (INACTIVE — use USERELATIONSHIP)
Dim_Budget → Dim_Date (INACTIVE — use USERELATIONSHIP)The most frequently queried fact table gets the active relationship.
Step 4: Write relationships.tmdl
// Star Schema Relationships
// === Fact_Usage (active relationships) ===
relationship Fact_Usage_to_Dim_Date
fromColumn: Fact_Usage.DateKey
toColumn: Dim_Date.Date
crossFilteringBehavior: oneDirection
relationship Fact_Usage_to_Dim_Subscription
fromColumn: Fact_Usage.SubscriptionGuid
toColumn: Dim_Subscription.SubscriptionGuid
crossFilteringBehavior: oneDirection
// === Fact_Marketplace (inactive — use USERELATIONSHIP) ===
relationship Fact_Marketplace_to_Dim_Date
fromColumn: Fact_Marketplace.DateKey
toColumn: Dim_Date.Date
isActive: false
crossFilteringBehavior: oneDirectionStep 5: Shared Dimensions
When the same dimension serves multiple fact tables:
1. Create the dimension once (derived from the primary fact table) 2. Ensure the FK column exists in all fact tables that reference it 3. Make the PQ expression source the primary fact for derivation 4. Add relationships from each fact table to the shared dimension
Step 6: Calendar Table (Dim_Date)
Every model needs a calendar table. Generate it in PQ covering the full date range:
- Start: earliest date in data (or a fixed start like 2024-01-01)
- End: latest date in data + 1 year
- Include: Year, Month, MonthName, Quarter, DayOfWeek, IsBusinessDay
- Localize: Portuguese month/day names for Brazilian projects
Mark the Date column as isKey: true in the TMDL.
Validation Checklist
- [ ] All relationships are M:1 (from fact to dimension)
- [ ] Cross-filtering is one-direction everywhere
- [ ] Only one active relationship per dimension-to-fact path
- [ ] Inactive relationships have corresponding USERELATIONSHIP measures
- [ ] Every fact table has a DateKey → Dim_Date relationship
- [ ] Dim_Date covers the full date range of all fact tables
- [ ] No circular relationships
- [ ] No bidirectional cross-filtering
- [ ] No snowflake chains (dimension → dimension)
Workflow: New PBIP Project
Scaffold a new Power BI Project (PBIP) from scratch with git-friendly structure.
Steps
1. Create Directory Structure
ProjectName/
├── ProjectName.pbip
├── ProjectName.Report/
│ ├── report.json
│ ├── definition.pbir
│ └── StaticResources/SharedResources/BaseThemes/
├── ProjectName.SemanticModel/
│ ├── definition.pbism
│ └── definition/
│ ├── model.tmdl
│ ├── tables/
│ ├── expressions/
│ └── relationships.tmdl
├── scripts/
└── docs/2. Create .pbip File
{
"version": "1.0",
"artifacts": [
{
"report": {
"path": "ProjectName.Report"
}
},
{
"dataset": {
"path": "ProjectName.SemanticModel"
}
}
]
}3. Create definition.pbism
{
"version": "1.0",
"settings": {}
}4. Create definition.pbir
{
"version": "1.0",
"datasetReference": {
"byPath": {
"path": "../ProjectName.SemanticModel"
}
}
}5. Create model.tmdl
model Model
culture: pt-BR
defaultPowerBIDataSourceVersion: powerBI_V3
sourceQueryCulture: pt-BRAdjust culture to match the project locale.
6. Create _Measures Table
In tables/_Measures.tmdl:
table _Measures
lineageTag: measures-001
column MeasureColumn
dataType: int64
isHidden: true
lineageTag: measures-col
summarizeBy: none
sourceColumn: none
partition _Measures = calculated
source = ROW("MeasureColumn", 0)7. Create Parameter Expression
In expressions/Parameter_ExportFolder.pq (if using folder-based ingestion):
"C:\data\exports" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true]8. Create relationships.tmdl
Start with a comment header:
// Star Schema Relationships - All Many-to-One from Facts to DimensionsAdd relationships as tables are created.
9. Create Theme (Optional)
In StaticResources/SharedResources/BaseThemes/CustomTheme.json:
{
"name": "CustomTheme",
"dataColors": ["#003366", "#0066CC", "#FF6600", "#339966", "#CC3333", "#9933CC"],
"background": "#FFFFFF",
"foreground": "#333333",
"tableAccent": "#0066CC"
}10. Initialize Git
git init
# Add .gitignore for PBI artifacts
echo "*.pbit" >> .gitignore
echo ".pbi/" >> .gitignore
echo "*.pbix" >> .gitignoreChecklist
- [ ] .pbip opens in Power BI Desktop
- [ ] model.tmdl has correct culture setting
- [ ] _Measures table exists with hidden column
- [ ] Parameter configured for data source
- [ ] relationships.tmdl created (even if empty)
- [ ] Git initialized with .gitignore
Workflow: Publish & Scheduled Refresh
Publish a PBIP report to Power BI Service and configure automated refresh.
Step 1: Publish from Desktop
1. Open the .pbip in Power BI Desktop 2. Verify data is refreshed and report looks correct 3. Home > Publish 4. Select destination workspace 5. If replacing existing, confirm overwrite
Step 2: Configure Data Gateway (for local/file data sources)
If the data source is local files (CSV folder), a network share, or on-prem database, you need the On-premises Data Gateway.
Install Gateway
1. Download from https://powerbi.microsoft.com/gateway/ 2. Install on a server that:
- Has access to the data source (CSV folder, file share, database)
- Is always on (not a laptop)
- Has reliable internet connection
3. Sign in with Power BI account during setup 4. Register the gateway in the Power BI Service tenant
Add Data Source to Gateway
1. Power BI Service > Settings (gear icon) > Manage gateways 2. Select your gateway > Add data source 3. For folder-based CSV:
- Data source type: Folder
- Path: the full path to the CSV folder (as seen from the gateway machine)
4. Configure credentials (Windows auth or organizational account)
Step 3: Configure Dataset in Service
1. Go to the workspace in Power BI Service 2. Find the dataset (semantic model) > Settings (gear icon) 3. Under Gateway connection:
- Map each data source to the gateway data source
- The folder parameter must match the gateway data source path
4. Under Data source credentials:
- Verify each source has valid credentials
- For Azure Cost Management connector: use organizational account with EA Admin role
5. Under Parameters:
- Verify
Parameter_ExportFoldervalue matches the gateway machine path
Step 4: Schedule Refresh
1. In dataset settings > Scheduled refresh 2. Toggle Keep your data up to date to ON 3. Set refresh frequency:
- Daily at 08:00 (typical for FinOps — after overnight export scripts run)
- Or multiple times per day if data changes frequently
4. Set timezone 5. Enable Send refresh failure notification to dataset owner 6. Apply
Step 5: Verify First Refresh
1. Trigger a manual refresh: dataset > Refresh now 2. Check Refresh history for success/failure 3. If failed, check error details:
- Gateway connectivity issues
- Credential problems
- Data source path mismatches
- Query timeout
Alternative: Direct API Connection (No Gateway)
If using the Azure Cost Management connector or other cloud connectors:
1. No gateway needed — PBI Service connects directly 2. In dataset settings, configure Data source credentials with OAuth2 3. The account must have appropriate Azure roles (EA Admin for EA connector) 4. Schedule refresh as above
Refresh Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| Gateway offline | Gateway service stopped | Restart on host machine |
| Credential expired | OAuth token expired | Re-enter credentials in dataset settings |
| File not found | Path mismatch | Verify Parameter_ExportFolder = gateway data source path |
| Query timeout | Large dataset or slow PQ | Optimize PQ queries, reduce date range |
| Access denied | Insufficient permissions | Verify credentials have read access to source |
| Parameter mismatch | Desktop vs Service parameter values | Update parameter value in Service dataset settings |
Automation with Python
Trigger refresh programmatically via Power BI REST API:
# Trigger refresh
curl -X POST \
"https://api.powerbi.com/v1.0/myorg/groups/{workspace-id}/datasets/{dataset-id}/refreshes" \
-H "Authorization: Bearer {access-token}" \
-H "Content-Type: application/json"Or use the export_billing_data.py script to export fresh CSVs, then let the scheduled refresh pick them up.
Workflow: Troubleshooting
Common Power BI issues and their solutions.
Data Loading Issues
| Problem | Diagnosis | Solution |
|---|---|---|
| "Cannot find folder" | Parameter_ExportFolder wrong | Transform Data > Edit Parameters > fix path |
| No data after refresh | CSV naming doesn't match PQ filter | Check Text.Contains filter in .pq matches actual filenames |
| Marketplace table empty | 2-header-row CSV not handled | PQ should skip first row + promote second as headers |
| Type conversion error | CSV has unexpected format | Verify UTF-8 encoding, check decimal separators (comma vs period) |
| Balance shows zeros | Balance CSV edited/corrupted | Re-export from EA portal, don't edit manually |
| Tags not parsed | Invalid JSON in Tags column | Verify Tags column contains valid JSON (not escaped strings) |
| Duplicate rows | Multiple CSVs for same period | Remove duplicate CSV files from export folder |
| Slow refresh | Large UsageDetails (>1M rows) | Filter by date range in PQ, or use incremental refresh |
TMDL Errors
| Problem | Diagnosis | Solution |
|---|---|---|
| "Duplicate lineageTag" | Two objects share same tag | Search all .tmdl files, make tags unique |
| "Column not found" | PQ output changed but TMDL not updated | Sync column names between .pq and .tmdl |
| "Invalid relationship" | FK column doesn't exist or type mismatch | Verify column names and types match in both tables |
| "Circular dependency" | Bidirectional or chained relationships | Remove bidirectional, flatten snowflake chains |
| Model won't load | Syntax error in TMDL | Check indentation (TMDL is whitespace-sensitive for measures) |
DAX Errors
| Problem | Diagnosis | Solution |
|---|---|---|
| "Circular dependency detected" | Measure references itself | Break the cycle, use intermediate measure |
| "Column not found" | Wrong table prefix or column name | Use TableName[ColumnName] format |
| Division by zero | Missing DIVIDE() or BLANK() check | Use DIVIDE(num, denom) or IF(denom <> 0, ...) |
| Wrong totals | Missing USERELATIONSHIP | Check if the relationship is inactive → add USERELATIONSHIP |
| Time intelligence wrong | Dim_Date not continuous | Ensure calendar has every date (no gaps) |
| SAMEPERIODLASTYEAR empty | No data for prior year | Extend Dim_Date range and verify data exists |
Power Query Debugging
Check PQ Output Before TMDL
1. Open .pbip in Power BI Desktop 2. Transform Data (Power Query Editor) 3. Click on the table name 4. Check Applied Steps — click each step to see intermediate results 5. Verify column names, types, and row counts
Common PQ Patterns That Break
// BAD: This silently returns empty if no files match
Table.SelectRows(Source, each Text.Contains([Name], "ExactWrongName"))
// GOOD: Add error handling or log
Table.SelectRows(Source, each
Text.Contains([Name], "UsageDetails", Comparer.OrdinalIgnoreCase))Encoding Issues
- EA portal exports are UTF-8 with BOM
- Python exports should use
encoding="utf-8" - In PQ:
Csv.Document(content, [Encoding=65001])for UTF-8
Relationship Debugging
Symptom: Slicer Doesn't Filter Visuals
1. Check relationship exists in relationships.tmdl 2. Check cross-filtering direction (should be oneDirection from fact to dim) 3. Check if relationship is active (isActive: false means it won't auto-filter) 4. If inactive, the measure must use USERELATIONSHIP
Symptom: Numbers Don't Add Up
1. Check for many-to-many relationships (should be M:1) 2. Check for bidirectional cross-filtering (should be one-direction) 3. Check if measure uses wrong relationship path 4. Verify DateKey in fact matches Date in Dim_Date (type and format)
Git / PBIP Issues
| Problem | Solution |
|---|---|
| report.json merge conflict | Accept one version, open in Desktop, re-save |
| .tmdl merge conflict | Manually merge (text-based, clean diffs) |
| PBI Desktop can't open .pbip | Check all referenced files exist, validate JSON |
| Changes lost after Desktop edit | Desktop may overwrite manual edits — close Desktop before editing files |
Performance
| Symptom | Fix |
|---|---|
| Slow refresh (>5 min) | Filter data in PQ (date range), reduce columns |
| Slow visuals | Reduce cardinality, avoid DISTINCTCOUNT on high-cardinality |
| Large file size | Remove unused columns in PQ, don't import raw JSON |
| Memory pressure | Split large fact tables by year, use aggregation tables |