Powerbi Modeling
github/awesome-copilot
Power BI semantic modeling assistant for building optimized data models.
TOM and ADOMD.NET guidance via PowerShell for connecting to Power BI Desktop's local Analysis Services instance.
$ npx skills add data-goblin/power-bi-agentic-development --skill connect-pbid -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install data-goblin/power-bi-agentic-development connect-pbid --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/data-goblin/power-bi-agentic-development.git skills-src && mkdir -p .claude/skills && cp -r skills-src/plugins/pbi-desktop/skills/connect-pbid .claude/skills/connect-pbid && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "connect-pbid" agent skill from https://github.com/data-goblin/power-bi-agentic-development/tree/main/plugins/pbi-desktop/skills/connect-pbid into .claude/skills/connect-pbid/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "connect-pbid", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/data-goblin/power-bi-agentic-development/tree/main/plugins/pbi-desktop/skills/connect-pbidType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add data-goblin/power-bi-agentic-development --skill connect-pbid -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install data-goblin/power-bi-agentic-development connect-pbid --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/data-goblin/power-bi-agentic-development.git skills-src && mkdir -p .agents/skills && cp -r skills-src/plugins/pbi-desktop/skills/connect-pbid .agents/skills/connect-pbid && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "connect-pbid" agent skill from https://github.com/data-goblin/power-bi-agentic-development/tree/main/plugins/pbi-desktop/skills/connect-pbid into .agents/skills/connect-pbid/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "connect-pbid", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add data-goblin/power-bi-agentic-development --skill connect-pbid -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install data-goblin/power-bi-agentic-development connect-pbid --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/data-goblin/power-bi-agentic-development.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/plugins/pbi-desktop/skills/connect-pbid .cursor/skills/connect-pbid && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "connect-pbid" agent skill from https://github.com/data-goblin/power-bi-agentic-development/tree/main/plugins/pbi-desktop/skills/connect-pbid into .cursor/skills/connect-pbid/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "connect-pbid", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/data-goblin/power-bi-agentic-development.git --path plugins/pbi-desktop/skills/connect-pbid--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add data-goblin/power-bi-agentic-development --skill connect-pbid -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install data-goblin/power-bi-agentic-development connect-pbid --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/data-goblin/power-bi-agentic-development.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/plugins/pbi-desktop/skills/connect-pbid .gemini/skills/connect-pbid && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "connect-pbid" agent skill from https://github.com/data-goblin/power-bi-agentic-development/tree/main/plugins/pbi-desktop/skills/connect-pbid into .gemini/skills/connect-pbid/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "connect-pbid", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install data-goblin/power-bi-agentic-development connect-pbidInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add data-goblin/power-bi-agentic-development --skill connect-pbid -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/data-goblin/power-bi-agentic-development.git skills-src && mkdir -p .github/skills && cp -r skills-src/plugins/pbi-desktop/skills/connect-pbid .github/skills/connect-pbid && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "connect-pbid" agent skill from https://github.com/data-goblin/power-bi-agentic-development/tree/main/plugins/pbi-desktop/skills/connect-pbid into .github/skills/connect-pbid/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "connect-pbid", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add data-goblin/power-bi-agentic-development --skill connect-pbid -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install data-goblin/power-bi-agentic-development connect-pbid --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/data-goblin/power-bi-agentic-development.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/plugins/pbi-desktop/skills/connect-pbid .opencode/skills/connect-pbid && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "connect-pbid" agent skill from https://github.com/data-goblin/power-bi-agentic-development/tree/main/plugins/pbi-desktop/skills/connect-pbid into .opencode/skills/connect-pbid/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "connect-pbid", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
connect-pbidTOM and ADOMD.NET guidance via PowerShell for connecting to Power BI Desktop's local Analysis Services instance.
Connect Pbid is an agent skill from data-goblin/power-bi-agentic-development. TOM and ADOMD.NET guidance via PowerShell for connecting to Power BI Desktop's local Analysis Services instance. Covers model enumeration, DAX queries, metadata modification, annotations, calendar definitions, field parameters, query tracing, DAX library package management (daxlib.org), and the Desktop Bridge for reloading and screenshotting the report canvas. Automatically invoke when the user mentions "Power BI Desktop", "Analysis Services port", "TOM", "ADOMD", "daxlib", "DAX library", "DAX UDF package", or…
Its SKILL.md is about 10k tokens, which your agent loads only when the skill is triggered. The skill folder holds 31 other files, including scripts and reference files (for example `daxlib.sh`, `references/annotations.md` and `references/calendar-column-groups.md`).
It sits in Data & Analytics. It works with Power BI, PowerShell and Model Context Protocol. The repository describes itself as: Power BI AI skills and Power BI agents for Claude Code and GitHub Copilot: a plugin marketplace of Power BI skills, subagents, and hooks for semantic models, DAX, TMDL, reports… The licence is GPL-3.0.
12 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit a301717. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships 2 files in scripts/ (Shell and PowerShell, from the files we listed), which the agent can run.
Shell commands in SKILL.md call:
wingetdotnetFrom the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
learn.microsoft.comdax.guideFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Connect Pbid loads about 10k tokens when it runs, and up to ~49k if it reads all its reference files. Until then it costs about 231 tokens; SKILL.md has 3,481 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); the scripts in this folder are not scanned.
The full file from data-goblin/power-bi-agentic-development at commit a301717, republished under its GPL-3.0 licence (© data-goblin). 3,481 words, ~10,132 tokens.
.claude/skills/connect-pbid/SKILL.md (or your agent's skills folder). This skill also uses 29 other files; get the full folder from GitHub.CRITICAL: Record mistakes, surprises, and model-specific nuances encountered while using this skill in
.claude/rules/connect-pbid.md. This file must begin with "Learnings from Claude about connecting to semantic models via the connect-pbid skill". Write only active reference notes (e.g. "QueryGroup property returns an object; access .Folder for the name string"); do not log a changelog or history of events. Omit anything already documented in the skill or its references. Keep the file under 1500 characters at all times; prune stale entries when adding new ones. Do not over-attend to this file; update it only when something genuinely unexpected is discovered.
Note: No MCP server is required. Use PowerShell with TOM/ADOMD.NET for the local model. When the report canvas is also in scope, pair it with
pbirfor report operations; never patch report JSON directly.
Expert guidance for connecting to Power BI Desktop's local tabular model via the Tabular Object Model (TOM) and ADOMD.NET in PowerShell. Covers connection, enumeration, DAX queries, query traces, and full model modification.
Activate only when the Tabular Editor CLI or a Power BI MCP server is unavailable. TOM is more reliable than direct TMDL editing because it validates changes against the engine and applies them atomically.
WARNING: This skill does NOT support remote models via the XMLA endpoint. For Direct Lake models or models hosted in Fabric, use the Tabular Editor CLI or a Power BI MCP server instead; the local Analysis Services proxy does not expose Direct Lake databases to external TOM/ADOMD.NET connections.
Power BI Desktop exposes the model and the report as two separate local surfaces. This skill owns the model surface and report-canvas verification, and routes report authoring to the right skill:
te CLI or a model MCP when available; fall back to this skill's TOM when they are not (see "When to Use This Skill").pbir-cli skill in the reports plugin (it drives the pbir CLI). The Desktop Bridge here only reloads and screenshots; it never edits visuals. Route every visual or page change to pbir-cli.pbir is unavailable): the pbir-format skill in the pbip plugin.Full loop on an open PBIP: change the model with TOM here, change visuals with pbir-cli, then reload and screenshot with the Desktop Bridge here to verify, and iterate.
localhostmsmdsrv.exe processes on different ports. Connect to each port, read $server.Databases[0].Name, and ask the user which model to work with if more than one is found. When the pbir CLI is installed, prefer pbir desktop list to map each Desktop PID to the exact file it has open (see Section 2a)Databases: 0 belongs to a thin report (live connection to a remote model); there is no local model to connect to. Query thin reports through their remote model instead (pbir model -q routes there automatically)$ variables ($env:TEMP, $server, etc.) silently. Two options: (1) single-quote the -Command arg so Bash passes $ literally to PowerShell; (2) write a .ps1 file with a heredoc (single-quoted delimiter preserves $) and use -File. On macOS via Parallels, the prlctl -> cmd.exe -> powershell.exe chain adds extra escaping layers; .ps1 files are more reliable for complex scripts but inline -Command with single quotes works for short commands.-ExecutionPolicy Bypass when running PowerShell commands or scripts. Windows blocks unsigned scripts by default..claude/scripts/, .github/scripts/, .cursor/scripts/, .gemini/scripts/, etc. depending on the harness). Ephemeral or throwaway scripts should go in a project tmp/ directory (which should be .gitignored). Do not write scripts to ./ root or /tmp/.$model.SaveChanges() to persist modifications; without it, changes are discardedhooks/config.yaml.| Requirement | Description |
|---|---|
| Power BI Desktop | Open with a model loaded (.pbix or .pbip) |
| PowerShell | Available on the machine running PBI Desktop |
| NuGet CLI | For package installation (winget install Microsoft.NuGet) |
| TOM NuGet Package | Microsoft.AnalysisServices.retail.amd64 -- model metadata |
| ADOMD.NET Package | Microsoft.AnalysisServices.AdomdClient.retail.amd64 -- DAX queries |
Install both packages only if not already present:
$pkgDir = "$env:TEMP\tom_nuget"
if (-not (Test-Path "$pkgDir\Microsoft.AnalysisServices.retail.amd64")) {
nuget install Microsoft.AnalysisServices.retail.amd64 -OutputDirectory $pkgDir -ExcludeVersion
}
if (-not (Test-Path "$pkgDir\Microsoft.AnalysisServices.AdomdClient.retail.amd64")) {
nuget install Microsoft.AnalysisServices.AdomdClient.retail.amd64 -OutputDirectory $pkgDir -ExcludeVersion
}Packages install DLLs under lib\net45\. Load with Add-Type -Path.
If a TOM operation fails with a compatibility level error or missing type, the
.retail.amd64package may be too old. A newer package (Microsoft.AnalysisServices, .NET 8+) ships with more recent TOM features. See daxlib.md for details on package differences.
Find the port, load TOM, connect, enumerate -- in one script:
# Find ports (deduped; netstat lists IPv4 and IPv6 entries per port)
$pids = (Get-Process msmdsrv -ErrorAction SilentlyContinue).Id
$ports = netstat -ano | Select-String "LISTENING" |
Where-Object { $pids -contains ($_ -split "\s+")[-1] } |
ForEach-Object { ($_ -split "\s+")[2] -replace ".*:" } |
Select-Object -Unique
# Load TOM
$basePath = "$env:TEMP\tom_nuget\Microsoft.AnalysisServices.retail.amd64\lib\net45"
Add-Type -Path "$basePath\Microsoft.AnalysisServices.Core.dll"
Add-Type -Path "$basePath\Microsoft.AnalysisServices.Tabular.dll"
# Connect to the first port that hosts a model; skip thin-report engines (0 databases)
$server = New-Object Microsoft.AnalysisServices.Tabular.Server
foreach ($p in $ports) {
$server.Connect("Data Source=localhost:$p")
if ($server.Databases.Count -eq 0) {
Write-Output "localhost:$p hosts no model (thin report); trying next port"
$server.Disconnect()
continue
}
break
}
$model = $server.Databases[0].Model
# Enumerate
foreach ($table in $model.Tables) {
Write-Output "TABLE: [$($table.Name)] ($($table.Columns.Count) cols, $($table.Measures.Count) measures)"
}
Write-Output "Relationships: $($model.Relationships.Count)"
$server.Disconnect()Port discovery methods:
| Method | Install Type | Command |
|---|---|---|
| Port file | Non-Store PBI Desktop | Get-Content "$env:LOCALAPPDATA\Microsoft\Power BI Desktop\AnalysisServicesWorkspaces\*\Data\msmdsrv.port.txt" |
| Port file | Store PBI Desktop | Get-Content "$env:LOCALAPPDATA\Packages\Microsoft.MicrosoftPowerBIDesktop_*\LocalState\AnalysisServicesWorkspaces\*\Data\msmdsrv.port.txt" |
| netstat | Any | netstat -ano | findstr LISTENING | findstr <PID> |
A port alone does not identify the report it serves; correlate before connecting to avoid modifying the wrong model. With the pbir CLI and Desktop's "external tool access" preview feature enabled, pbir desktop list shows each Desktop PID with the exact file it has open. Map ports to those PIDs through the process tree (each msmdsrv.exe is a child of its PBIDesktop.exe):
$conns = Get-NetTCPConnection -State Listen
foreach ($proc in Get-Process msmdsrv -ErrorAction SilentlyContinue) {
$port = ($conns | Where-Object OwningProcess -eq $proc.Id | Select-Object -First 1).LocalPort
$parent = (Get-WmiObject Win32_Process -Filter "ProcessId=$($proc.Id)").ParentProcessId
Write-Output "port $port -> msmdsrv $($proc.Id) -> PBIDesktop $parent"
}An engine reporting Databases: 0 is a thin report's workspace; no local model exists. Query the remote model instead (pbir model -q routes there automatically).
$basePath = "$env:TEMP\tom_nuget\Microsoft.AnalysisServices.retail.amd64\lib\net45"
Add-Type -Path "$basePath\Microsoft.AnalysisServices.Core.dll"
Add-Type -Path "$basePath\Microsoft.AnalysisServices.Tabular.dll"
Add-Type -Path "$basePath\Microsoft.AnalysisServices.Tabular.Json.dll"$server = New-Object Microsoft.AnalysisServices.Tabular.Server
$server.Connect("Data Source=localhost:<PORT>")
# PBI Desktop always has exactly one database
$db = $server.Databases[0]
$model = $db.ModelOnly save after all changes are made. After modifications, persist with:
$model.SaveChanges()Changes appear immediately in PBI Desktop. The user cannot undo with Ctrl+Z in Power BI, which is a disadvantage of this approach.
IMPORTANT: Remember to disconnect after modifications are done. NEVER remain connected, which can lead to orphaned processes.
$server.Disconnect()Write-Output "Server: $($server.Name)"
Write-Output "Version: $($server.Version)"
Write-Output "Database: $($db.Name)"
Write-Output "Compatibility: $($db.CompatibilityLevel)"Trigger a data refresh via TMSL (Tabular Model Scripting Language) or TOM's RequestRefresh API. This re-executes Power Query/M expressions and reloads data into the VertiPaq engine.
# Full refresh of a single table via TMSL
$dbName = $server.Databases[0].Name
$tmsl = '{ "refresh": { "type": "full", "objects": [{ "database": "' + $dbName + '", "table": "Sales" }] } }'
$server.Execute($tmsl)
# Or via TOM RequestRefresh API
$model.Tables["Sales"].RequestRefresh([Microsoft.AnalysisServices.Tabular.RefreshType]::Full)
$model.SaveChanges()| Refresh Type | Behaviour |
|---|---|
full | Drop data, re-query source, recalculate DAX |
calculate | Recalculate DAX only (no source query) |
automatic | Engine decides per-partition what's needed |
dataOnly | Re-query source but skip DAX recalculation |
For detailed examples and all refresh methods, see refresh-model.md.
Add-Type -Path "$env:TEMP\tom_nuget\Microsoft.AnalysisServices.AdomdClient.retail.amd64\lib\net45\Microsoft.AnalysisServices.AdomdClient.dll"$conn = New-Object Microsoft.AnalysisServices.AdomdClient.AdomdConnection
$conn.ConnectionString = "Data Source=localhost:<PORT>"
$conn.Open()All queries should preferably use SUMMARIZECOLUMNS.
Check dax.guide online for information about DAX functions, if necessary.
Important: ADOMD.NET returns fully-qualified column names without quotes around the table name (e.g., Brands[Brand Class] not Brand Class; measure projections come back as [@Alias]). Do not access columns by short name ($reader["Brand Class"]) -- it fails silently and returns blank. Use $reader.GetName($i) to discover column names, then access by index:
$cmd = $conn.CreateCommand()
$cmd.CommandText = "EVALUATE SUMMARIZECOLUMNS('Table'[Column], ""@MeasureName"", [Measure])"
$reader = $cmd.ExecuteReader()
# Always iterate by index and use GetName() to map columns
while ($reader.Read()) {
for ($i = 0; $i -lt $reader.FieldCount; $i++) {
Write-Output "$($reader.GetName($i)): $($reader.GetValue($i))"
}
Write-Output "---"
}
$reader.Close()'Sales'[Amount], not [Amount]. This applies everywhere -- measures, calculated columns, queries. Unqualified columns cause ambiguity errors.'Sales'[Amount], 'D&D 5E Monsters'[CR]. Even simple names like Sales should be quoted as 'Sales' for consistency.[Total Revenue]"" inside PowerShell here-strings# Full table scan
$cmd.CommandText = "EVALUATE 'Sales'"
# Filtered with CALCULATETABLE
$cmd.CommandText = "EVALUATE CALCULATETABLE('Sales', 'Sales'[Region] = ""West"")"
# Aggregation
$cmd.CommandText = "EVALUATE SUMMARIZECOLUMNS('Date'[Year], ""@Total"", SUM('Sales'[Amount]))"
# Scalar via ROW
$cmd.CommandText = "EVALUATE ROW(""Result"", COUNTROWS('Sales'))"
# DMV queries (model metadata via SQL-like syntax)
$cmd.CommandText = "SELECT * FROM `$SYSTEM.TMSCHEMA_TABLES"
$cmd.CommandText = "SELECT * FROM `$SYSTEM.TMSCHEMA_MEASURES"
$cmd.CommandText = "SELECT * FROM `$SYSTEM.TMSCHEMA_COLUMNS"
$cmd.CommandText = "SELECT * FROM `$SYSTEM.TMSCHEMA_RELATIONSHIPS"$conn.Close()All modifications require a TOM connection (section 3). Call $model.SaveChanges() after each batch of changes.
For full CRUD examples of every object type, see tom-object-types.md.
Common object types and their TOM collections (not exhaustive -- see Microsoft TOM API docs for the full namespace):
| Object | Collection | Create | Read | Update | Delete |
|---|---|---|---|---|---|
| Table | $model.Tables | New-Object ...Table | $model.Tables["Name"] | Set properties | .Remove($obj) |
| Column | $table.Columns | New-Object ...DataColumn | $table.Columns["Name"] | Set properties | .Remove($obj) |
| Measure | $table.Measures | New-Object ...Measure | $table.Measures["Name"] | Set properties | .Remove($obj) |
| Calculated Column | $table.Columns | New-Object ...CalculatedColumn | Filter by type | Set .Expression | .Remove($obj) |
| Calculated Table | $model.Tables | Table + calculated partition | Check partition type | Set partition expr | .Remove($obj) |
| Relationship | $model.Relationships | New-Object ...SingleColumnRelationship | Index or filter | Set properties | .Remove($obj) |
| Hierarchy | $table.Hierarchies | New-Object ...Hierarchy | $table.Hierarchies["Name"] | Modify levels | .Remove($obj) |
| Role | $model.Roles | New-Object ...ModelRole | $model.Roles["Name"] | Set permissions | .Remove($obj) |
| Perspective | $model.Perspectives | New-Object ...Perspective | $model.Perspectives["Name"] | Toggle membership | .Remove($obj) |
| Culture | $model.Cultures | New-Object ...Culture | $model.Cultures["en-US"] | Set translations | .Remove($obj) |
| Partition | $table.Partitions | New-Object ...Partition | $table.Partitions["Name"] | Set source/expression | .Remove($obj) |
| Annotation | Any object | $obj.Annotations.Add(...) | $obj.Annotations["Key"] | Set .Value | .Remove($obj) |
| Expression | $model.Expressions | New-Object ...NamedExpression | $model.Expressions["Name"] | Set .Expression | .Remove($obj) |
| Data Source | $model.DataSources | New-Object ...StructuredDataSource | $model.DataSources["Name"] | Set connection | .Remove($obj) |
| Calculation Group | $model.Tables | Table with CalculationGroup | Filter by type | Add/remove items | .Remove($obj) |
Quick examples (inline):
# Add a measure
$m = New-Object Microsoft.AnalysisServices.Tabular.Measure
$m.Name = "Total Revenue"
$m.Expression = "SUM(Sales[Amount])"
$m.FormatString = "`$#,0"
$m.Description = "Sum of all sales amounts"
$model.Tables["Sales"].Measures.Add($m)
# Add a relationship
$rel = New-Object Microsoft.AnalysisServices.Tabular.SingleColumnRelationship
$rel.Name = "Sales_to_Date"
$rel.FromColumn = $model.Tables["Sales"].Columns["DateKey"]
$rel.ToColumn = $model.Tables["Date"].Columns["DateKey"]
$rel.FromCardinality = [Microsoft.AnalysisServices.Tabular.RelationshipEndCardinality]::Many
$rel.ToCardinality = [Microsoft.AnalysisServices.Tabular.RelationshipEndCardinality]::One
$model.Relationships.Add($rel)
# Rename a column
$model.Tables["Sales"].Columns["amt"].Name = "Amount"
# Hide a table
$model.Tables["Bridge"].IsHidden = $true
# Delete a measure
$m = $model.Tables["Sales"].Measures["Old Measure"]
$model.Tables["Sales"].Measures.Remove($m)
# Add a role with RLS
$role = New-Object Microsoft.AnalysisServices.Tabular.ModelRole
$role.Name = "Region Filter"
$role.ModelPermission = [Microsoft.AnalysisServices.Tabular.ModelPermission]::Read
$model.Roles.Add($role)
$tp = New-Object Microsoft.AnalysisServices.Tabular.TablePermission
$tp.Table = $model.Tables["Sales"]
$tp.FilterExpression = "[Region] = USERNAME()"
$role.TablePermissions.Add($tp)
$model.SaveChanges()For complete TOM object type tables, PowerShell reflection patterns for discovering properties and enum values, and reading/setting property examples, see references/tom-object-types.md.
Before saving measure/column expressions, validate them by test-executing against the live model. This catches syntax errors, missing column references, and circular dependencies without persisting bad metadata.
# Validate a DAX expression before adding it as a measure
$testExpr = "SUM('Sales'[Amount]) / COUNTROWS('Sales')"
$cmd = $conn.CreateCommand()
$cmd.CommandText = "EVALUATE ROW(`"@Test`", $testExpr)"
try {
$reader = $cmd.ExecuteReader()
$reader.Close()
Write-Output "VALID"
} catch {
Write-Output "INVALID: $($_.Exception.Message)"
}For calculated table expressions, wrap in COUNTROWS:
$tableExpr = "CALENDAR(DATE(2020,1,1), DATE(2030,12,31))"
$cmd.CommandText = "EVALUATE ROW(`"@Count`", COUNTROWS($tableExpr))"For filter expressions (RLS), test with CALCULATETABLE:
$filterExpr = "'Sales'[Region] = `"West`""
$cmd.CommandText = "EVALUATE CALCULATETABLE(ROW(`"@OK`", 1), $filterExpr)"SaveChanges() applies all pending modifications in a single implicit transaction. If any object fails validation, the entire batch rolls back automatically.
For multi-step workflows where inspection or rollback is needed before committing:
try {
# Make changes (not yet persisted)
$model.Tables["Sales"].Measures["Revenue"].Name = "Total Revenue"
$model.Tables["Sales"].Measures["Cost"].Name = "Total Cost"
# Inspect before committing (changes are local to this connection)
foreach ($m in $model.Tables["Sales"].Measures) {
Write-Output " [$($m.Name)]"
}
# Commit all changes atomically
$model.SaveChanges()
Write-Output "Committed"
} catch {
# Discard all uncommitted changes
$model.UndoLocalChanges()
Write-Output "Rolled back: $($_.Exception.Message)"
}UndoLocalChanges() discards all modifications made since the last SaveChanges(). This is the rollback mechanism for PBI Desktop; there is no explicit begin/commit transaction API on the local Analysis Services instance.
The TOM API does not expose a public Validate() method. Validation happens implicitly during SaveChanges() (which rolls back the entire batch on failure). For pre-save validation, inspect objects manually:
# Check measures have valid expressions (non-empty)
foreach ($m in ($model.Tables | ForEach-Object { $_.Measures }) ) {
if ([string]::IsNullOrWhiteSpace($m.Expression)) {
Write-Output "WARNING: Measure [$($m.Name)] in [$($m.Table.Name)] has no expression"
}
}
# Check relationships reference valid columns
foreach ($rel in $model.Relationships) {
$sr = [Microsoft.AnalysisServices.Tabular.SingleColumnRelationship]$rel
if ($sr.FromColumn -eq $null -or $sr.ToColumn -eq $null) {
Write-Output "WARNING: Relationship [$($sr.Name)] has null column references"
}
}
# Check for duplicate measure names across tables
$names = @{}
foreach ($m in ($model.Tables | ForEach-Object { $_.Measures })) {
if ($names.ContainsKey($m.Name)) {
Write-Output "WARNING: Duplicate measure name [$($m.Name)] in [$($m.Table.Name)] and [$($names[$m.Name])]"
}
$names[$m.Name] = $m.Table.Name
}TOM does not expose the .pbix/.pbip file path directly.
Primary method — Desktop bridge: pbir desktop list reports the exact file each running instance has open (requires the pbir CLI and the "external tool access" preview feature; see Section 2a). Use the methods below only when that is unavailable.
Fallback — FileHistory in User.zip (works for Store and non-Store):
# Read the most recently opened file from PBI Desktop's settings
$userZip = "$env:USERPROFILE\Microsoft\Power BI Desktop Store App\User.zip"
# For non-Store installs: "$env:LOCALAPPDATA\Microsoft\Power BI Desktop\User.zip"
Add-Type -Assembly System.IO.Compression.FileSystem
$z = [System.IO.Compression.ZipFile]::OpenRead($userZip)
$entry = $z.Entries | Where-Object { $_.Name -eq 'Settings.xml' }
$reader = New-Object System.IO.StreamReader($entry.Open())
$content = $reader.ReadToEnd()
$reader.Close()
$z.Dispose()
# Extract FileHistory entries (ordered by lastAccessedDate, most recent first)
$history = ($content -split '(?=<Entry)' | Where-Object { $_ -match 'FileHistory' })[0]
$json = [regex]::Match($history, 'Value="s\[(.*?)\]"').Groups[1].Value -replace '"', '"'
$files = $json | ConvertFrom-Json
$files | Select-Object filePath, lastAccessedDate | Format-Table -AutoSizeThe first entry is the most recently opened file. Files on the Mac (via Parallels) appear as \\Mac\Home\... paths.
Limitation: This is an imperfect method — it reads recent file history, not the currently open file. If multiple PBI Desktop instances are open, or the most recently accessed file in history isn't the one currently open, the result may be wrong. Confirm with the user if there is any ambiguity.
Fallback — window title (non-Store PBI Desktop only):
Get-Process PBIDesktop -ErrorAction SilentlyContinue | Select-Object Id, MainWindowTitleNote: Store PBI Desktop (from Microsoft Store / WindowsApps) does not expose the file path in the window title — use the User.zip method above instead.
Fallback — msmdsrv command line (gives workspace path, not file path):
# Useful for finding the port; does NOT reveal the source file path
(Get-WmiObject Win32_Process -Filter "Name='msmdsrv.exe'").CommandLineFor .pbip projects, metadata files are human-readable JSON/TMDL on disk and can be read and modified directly.
Common targets:
| File | Purpose | Skill |
|---|---|---|
<Name>.Report/definition.pbir | Report-to-model connection (byPath or byConnection) | pbip |
<Name>.Report/definition/report.json | Report-level settings, theme, filters | pbir-format |
<Name>.SemanticModel/definition/*.tmdl | Model metadata (tables, measures, relationships) | tmdl |
<Name>.SemanticModel/definition/expressions.tmdl | M/Power Query shared expressions and parameters | tmdl |
For syntax, structure, and editing patterns for these files, load the relevant skill from the pbip plugin:
pbip -- project structure, file types, .pbir connection, forkingpbir-format -- report.json, visual.json, themes, filters, PBIR JSON schemastmdl -- TMDL syntax, measures, columns, roles, relationshipsPower BI Desktop does not watch for external file changes; edits made on disk while a report is open are silently ignored or overwritten on the next Desktop save. To apply changes, in order of preference:
$model.SaveChanges()) apply to the running instance immediately. Prefer this for model metadata.pbir desktop refresh "Report.Report" (PBIP/PBIR only, not .pbix; requires the preview feature). Theme JSON edits under StaticResources do NOT hot-reload; close and reopen instead. If the instance has unsaved changes, Desktop saves first and may overwrite the on-disk edit..pbix): close Power BI Desktop, edit, reopen.For report (PBIR) files specifically, the Desktop Bridge reloads on-disk edits into the open canvas without reopening (the file.reload/v1 pipe method, with the powerbi-desktop npm CLI as a fallback); see section 13. Model (TMDL) on-disk edits still require close-and-reopen, or use live TOM SaveChanges() as above.
| Topic | URL |
|---|---|
| TOM API Reference | learn.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.tabular |
| TOM Overview | learn.microsoft.com/en-us/analysis-services/tom/introduction-to-the-tabular-object-model-tom-in-analysis-services-amo |
| ADOMD.NET Reference | learn.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.adomdclient |
| Client Libraries | learn.microsoft.com/en-us/analysis-services/client-libraries |
| DMV Reference | learn.microsoft.com/en-us/analysis-services/instances/use-dynamic-management-views-dmvs-to-monitor-analysis-services |
| DAX Reference | dax.guide |
| Compatibility Levels | learn.microsoft.com/en-us/analysis-services/tabular-models/compatibility-level-for-tabular-models-in-analysis-services |
To retrieve current TOM/ADOMD.NET reference docs, use microsoft_docs_search + microsoft_docs_fetch (MCP) if available, otherwise mslearn search + mslearn fetch (CLI). Search based on the user's request and run multiple searches as needed to ensure sufficient context before proceeding.
EVALUATEANDLOG(<Value>, [Label], [MaxRows]) wraps any DAX expression, returns it unchanged, and emits intermediate results as JSON via a trace event. Works in PBI Desktop only.
Programmatic capture via the TOM Trace API eliminates the need for external tools (DAX Debug Output, SQL Server Profiler, DAX Studio). Subscribe to the DAXEvaluationLog trace event (enum ID 135), capture events with a synchronized ArrayList via Register-ObjectEvent, and parse the JSON from $Event.SourceEventArgs.TextData.
Critical implementation detail: Register-ObjectEvent -Action runs in a separate PowerShell runspace. $global: variables inside the action block do not share scope. Pass a synchronized collection via -MessageData:
$evalEvents = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$job = Register-ObjectEvent -InputObject $trace -EventName "OnEvent" -MessageData $evalEvents -Action {
$Event.MessageData.Add($Event.SourceEventArgs) | Out-Null
}Trace delivery is asynchronous: DAXEvaluationLog events typically arrive 2-3.5 seconds after the query returns, so a short fixed sleep misses them. Poll the captured-event count (up to ~10s in 500ms steps) before reading results. Warm-cache runs still emit the event; do not rely on cache clearing to make it fire. Clear the VertiPaq cache only when cold-cache timings are needed:
$server.Execute('{ "clearCache": { "object": { "database": "' + $db.Name + '" } } }') | Out-NullCommon debugging patterns:
| Pattern | Approach |
|---|---|
| Measure chain decomposition | Wrap each intermediate step: EVALUATEANDLOG([Step1], "Label1") |
| Filter context inspection | Trace CALCULATE with vs without ALL/REMOVEFILTERS |
| BLANK vs zero detection | Trace the value before a comparison; BLANK = 0 is TRUE in DAX |
| Variable context trap | Trace VAR value alongside CALCULATE result; proves VAR is not re-evaluated |
| Grand total diagnosis | Trace numerator + denominator at row vs total grain |
| Table expression inspection | Wrap CALCULATETABLE result; trace shows actual rows feeding an aggregate |
For full setup, JSON payload structure, event batching behavior, and all debugging patterns, see evaluateandlog-debugging.md.
Programmatic equivalent of DAX Studio's Server Timings. Subscribe to QueryEnd, VertiPaqSEQueryEnd, and VertiPaqSEQueryCacheMatch trace events to measure Formula Engine (FE) vs Storage Engine (SE) time per query.
Key formula: FE time = Total duration - sum(SE scan durations)
Important: VertiPaqSEQueryCacheMatch does NOT support Duration or CpuTime columns; adding them causes $trace.Update() to throw. Only add TextData + EventClass for cache match events.
Workflow:
clearCache) for cold timingsQueryEnd for total, VertiPaqSEQueryEnd for per-scan SE durationsStatistical sampling: Single measurements are noisy. Always take 6-12 samples and compare medians (not means) before and after a change. If ranges overlap significantly, the difference is likely noise. Discard the first cold-cache run as warm-up. See the reference for a Measure-QueryMedian helper.
Visual query profiling: Construct SUMMARIZECOLUMNS queries from PBIR visual.json definitions. Column projections become group-by columns; measure projections become measure references; Aggregation.Function maps to SUM (0), MIN (1), MAX (2), COUNT (3), AVERAGE (4).
For full setup, timing interpretation, sampling patterns, and PBIR-to-DAX translation, see performance-profiling.md.
The TOM connection above drives the model: tables, measures, relationships, roles, refresh. It cannot touch the report canvas (pages and visuals). Power BI Desktop exposes a second, separate local API for that: the Desktop Bridge, a per-process JSON-RPC server on the Windows named pipe \\.\pipe\pbi-desktop-bridge-<PID>. Pair the two to change the model and immediately confirm the report re-renders.
When the pbir CLI is installed, it wraps this same pipe; prefer it over driving the pipe raw:
pbir desktop list # PID + open file per instance
pbir model "Report.Report" -q 'EVALUATE ROW("Check", [New Measure])' # engine-level check
pbir desktop refresh "Report.Report" # reload on-disk PBIR into the canvas
pbir desktop screenshot "Report.Report/Page Name.Page" -o verify.png # inspect renderingThe single-quoted PowerShell argument preserves the embedded DAX quotes without a shell-specific stop-parsing token.
Without pbir, drive the pipe raw from PowerShell, the same way this skill drives TOM/ADOMD. It requires the Desktop bridge preview setting enabled (File > Options and settings > Options > Preview features, then restart). Auto-discover the PID by enumerating the pipe directory; then over JSON-RPC: application.state.get/v1 returns the open file path (currentFilePath, so the bridge can locate the PBIP on disk), file.reload/v1 reloads the on-disk PBIR into the canvas, and report.snapshot.capture/v1 returns a page PNG.
Model-plus-report loop: edit the model with TOM and $model.SaveChanges() (applies live), then reload and screenshot the report to confirm visuals reflect the change (a renamed measure, a new format string, a repaired relationship). On-disk report (PBIR) edits are picked up by reload; on-disk model (TMDL) edits and theme files under StaticResources still need a reopen, so prefer live TOM for model changes. The bridge drives the Windows app, so on macOS run it inside the Parallels VM (see parallels-macos.md).
For the full command set, PID selection, the JSON-RPC method surface (bridge.manifest, application.state.get/v1, file.reload/v1, report.snapshot.capture/v1), and how it complements the Analysis Services local API, see desktop-bridge.md. To CHANGE visuals, pages, formatting, filters, or bookmarks, route to the pbir-cli skill (reports plugin); the Desktop Bridge here only reloads and screenshots, it never edits the report.
Alternative path (only if driving the raw pipe runs into trouble, framing, encoding, or a build that changed a param shape): use the pbir desktop commands (reports plugin pbir-cli skill), which wrap these same methods. See desktop-bridge.md.
Skill references:
pbir desktop commands); pairing model (TOM) edits with report verificationCLI tools at the skill root:
daxlib -- CLI for browsing, downloading, and installing DAX library packages from daxlib.org. Script at daxlib.sh (requires bash + jq). Model operations (add/update/remove) call scripts/daxlib-tom/ via dotnet run (requires .NET 8 SDK). On macOS, model operations route through Parallels automatically. See daxlib.md for full command reference.Agents:
query-listener -- Dispatch to capture live visual DAX queries in real time; polls DISCOVER_SESSIONS and reports query text + timingsExample scripts in scripts/:
connect-and-enumerate.ps1 - Connect to PBI Desktop and list all tables, columns, measures, relationshipsexplore-model.ps1 - Hierarchical metadata enumeration (tables, columns, measures, hierarchies, partitions, relationships, roles, perspectives, cultures, expressions, data sources)query-dax.ps1 - Execute DAX queries via ADOMD.NET with formatted outputrefresh-table.ps1 - Refresh a table or entire model via TMSL with configurable refresh typemodify-tom-objects.ps1 - Create table, rename measures, set folders/formats, hide columns, create relationship (with undo)create-field-parameter.ps1 - Create a field parameter table from a list of measures with all required metadatadebug-dax.ps1 - Debug DAX with EVALUATEANDLOG trace capture and performance timings; auto-detects port, enumerates model measures, provides Invoke-DebugQuery helperload-tmdl.ps1 - Load a local TMDL folder or BIM file into TOM offline (no running engine), enumerate the model, optionally add a measure and save backconnect-from-mac.sh - macOS wrapper that runs PowerShell scripts in a Parallels VM via prlctl execExternal references:
dax.guide/<function>/ for individual function reference© data-goblin, GPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 29 other files (scripts, references) in plugins/pbi-desktop/skills/connect-pbid of data-goblin/power-bi-agentic-development.
Open the folder on GitHubat commit a301717
Connect Pbid next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Connect Pbid this skilldata-goblin/power-bi-agentic-development | 1k | — | ~10k | Automated safety check: Pass | GPL-3.0 | |
| Powerbi Modelinggithub/awesome-copilot | 40k | 1 repos | ~1.5k | Automated safety check: Pass | MIT | |
| Powerbi MCPKilo-Org/kilo-marketplace | 190 | — | ~1.7k | Automated safety check: Pass | MIT | |
| Implementing Data Loss Prevention With Microsoft Purviewmukul975/Anthropic-Cybersecurity-Skills | 34k | — | ~7.9k | Automated safety check: Pass | Apache-2.0 | |
| Azure Resource GraphMicrosoftDocs/Agent-Skills | 777 | — | ~1.8k | Automated safety check: Pass | CC-BY-4.0 | |
| Ketch1broseidon/ketch | 700 | 1 repos | ~3.9k | Automated safety check: Pass | MIT |
github/awesome-copilot
Power BI semantic modeling assistant for building optimized data models.
Kilo-Org/kilo-marketplace
Safety and governance protocol for Power BI semantic model sessions via MCP.
mukul975/Anthropic-Cybersecurity-Skills
Implements DLP policies using Microsoft Purview PowerShell cmdlets and the Graph API to protect data across Exchange Online, SharePoint, OneDrive, Teams, endpoints, and Power BI, including…
MicrosoftDocs/Agent-Skills
Expert knowledge for Azure Resource Graph development including troubleshooting, best practices, decision making, limits & quotas, configuration, and integrations & coding patterns.
1broseidon/ketch
Research skill for ketch — a fast stateless CLI for web search, OSS code search, curated library docs, page scraping, and site crawling; an optional MCP server exists for operators who want it, but…
timescale/pg-aiguide
A skill your agent uses to analyze an existing PostgreSQL database and identify which tables should be converted to Timescale/TimescaleDB hypertables.
data-goblin/power-bi-agentic-development
Author, validate, publish, and test Power BI paginated reports in the RDL format.
data-goblin/power-bi-agentic-development
Automatically invoke this skill whenever the user asks about Fabric tenant settings or Power BI tenant settings or auditing tenant settings.
data-goblin/power-bi-agentic-development
Interactive BPA rule generation for Power BI semantic models; guided discovery, model investigation, and expert rule authoring.
data-goblin/power-bi-agentic-development
Guidance for Power BI Project (PBIP) structure, thick and thin reports, project renames, forks, and validation.
data-goblin/power-bi-agentic-development
Actionable feedback on the quality, usage, and effectiveness of Power BI reports.
data-goblin/power-bi-agentic-development
This skill should be used whenever the user mentions a "semantic model", "data model", or "dataset", or asks to "build", "model", "design", "optimize", "review", or "audit" one, or to "add a…
Works with
Categories
TOM and ADOMD.NET guidance via PowerShell for connecting to Power BI Desktop's local Analysis Services instance. Connect Pbid is an agent skill from data-goblin/power-bi-agentic-development.NET guidance via PowerShell for connecting to Power BI Desktop's local Analysis Services instance.
Connect Pbid fits situations like: mentions Power BI Desktop; analysis Services port; DAX UDF package; asks to connect to PBI Desktop.
Run `npx skills add data-goblin/power-bi-agentic-development --skill connect-pbid -a claude-code`. Or copy the skill folder (plugins/pbi-desktop/skills/connect-pbid in data-goblin/power-bi-agentic-development) into .claude/skills/connect-pbid in your project. Claude Code loads it when a task matches its description.
Run `npx skills add data-goblin/power-bi-agentic-development --skill connect-pbid -a codex`. Or copy the skill folder (plugins/pbi-desktop/skills/connect-pbid in data-goblin/power-bi-agentic-development) into .agents/skills/connect-pbid in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add data-goblin/power-bi-agentic-development --skill connect-pbid -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/connect-pbid, .gemini/skills/connect-pbid, .github/skills/connect-pbid and .opencode/skills/connect-pbid in your project.
Going by SKILL.md and its folder, Connect Pbid needs a shell and PowerShell for the scripts in its folder and the command-line tools its instructions call (winget and dotnet). Our summary lists: A Bash shell; PowerShell.
SKILL.md names 2 domains. As links in the text: learn.microsoft.com and dax.guide. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. The check reads SKILL.md only: the scripts in the folder are not scanned, so read them before running anything.
Connect Pbid is published under the GPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 10k tokens (SKILL.md is roughly 41k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 39k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Connect Pbid: Powerbi Modeling (github/awesome-copilot, 40k stars), Powerbi MCP (Kilo-Org/kilo-marketplace, 190 stars), Implementing Data Loss Prevention With Microsoft Purview (mukul975/Anthropic-Cybersecurity-Skills, 34k stars) and Azure Resource Graph (MicrosoftDocs/Agent-Skills, 777 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
data-goblin (a GitHub user) maintains it in data-goblin/power-bi-agentic-development, which has 1,034 GitHub stars. The repository holds 33 skills in this directory. The repository was last updated on October 7, 2026.
Source: data-goblin/power-bi-agentic-development on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.