agentsclimarketplace

Pbid tom live

Skill wardawgmalvicious/claude-config/skills/pbid-tom-live

Personal Claude Code config — skills, subagents, hooks, and rules for Microsoft Fabric and Power BI workflows on Windows. Cherry-pickable, no semver.

Install
npx -y skills add wardawgmalvicious/claude-config --skill pbid-tom-live

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use for scripting an open Power BI Desktop model via its localhost msmdsrv.exe Analysis Services proxy — TOM (Microsoft.AnalysisServices.Tabular) for metadata, ADOMD.NET (AdomdConnection) for DAX. Covers PowerShell setup with retail.amd64 AMO/TOM + ADOMD NuGet DLLs, discovering the msmdsrv port (msmdsrv.port.txt / netstat), connecting via server Databases Model, SaveChanges / UndoLocalChanges, ExecuteReader for EVALUATE, DAX validation pre-save, TMSL refresh types (full / calculate / automatic / dataOnly / clearValues / defragment), VertiPaq DMVs ($SYSTEM.DISCOVER_STORAGE_TABLE_COLUMN_SEGMENTS, TMSCHEMA_*), query listener via DISCOVER_SESSIONS, EVALUATEANDLOG trace events (DAXEvaluationLog), FE-vs-SE Server Timings profiling, DAXLib UDF packages (CL 1702+), Calendar Column Groups (CL 1604+). Invoke when user mentions TOM, ADOMD, msmdsrv, VertiPaq, DMV, EVALUATEANDLOG, Server Timings, daxlib.

SKILL.md

18.2 KB, as published. Nobody here has run it

Power BI Desktop — Live TOM / ADOMD Operations

Local Analysis Services proxy inside Power BI Desktop. Enables TOM (metadata) and ADOMD.NET (DAX query) connections from PowerShell. Does not work for thin reports, Direct Lake models, or remote XMLA endpoints. For those use a remote XMLA / Power BI REST approach — see fabric-tmdl for semantic-model authoring and fabric-tmdl-api for Fabric semantic-model definition REST call patterns (createItemWithDefinition, updateDefinition).

Note (April 2026): Direct Lake on OneLake gained calculated-column / calculated-table preview support. The local msmdsrv proxy still does not bind to Direct Lake models — author and edit those columns through the remote XMLA endpoint (or the Desktop UI) and use TOM there. Local TOM / ADOMD scripting in this skill remains scoped to Import / DirectQuery models.

Prerequisites

RequirementNotes
Power BI DesktopOpen with a non-thin model loaded
PowerShell + nugetwinget install Microsoft.NuGet
Microsoft.AnalysisServices.retail.amd64TOM metadata DLLs
Microsoft.AnalysisServices.AdomdClient.retail.amd64DAX query DLLs

Packages extract to $env:TEMP\tom_nuget\...\lib\net45\. Load via Add-Type -Path. If TOM calls fail with missing type / compatibility level errors, swap to the newer Microsoft.AnalysisServices (.NET 8) package.

Discovering Running PBID Instances

Each open .pbix/.pbip spawns one msmdsrv.exe on a random localhost port. When multiple are running, connect to each, read $server.Databases[0].Name, and ask the user which to target.

MethodWorks OnCommand
Port file (non-Store)Desktop installerGet-Content "$env:LOCALAPPDATA\Microsoft\Power BI Desktop\AnalysisServicesWorkspaces\*\Data\msmdsrv.port.txt"
Port file (Store)MS Store installGet-Content "$env:LOCALAPPDATA\Packages\Microsoft.MicrosoftPowerBIDesktop_*\LocalState\AnalysisServicesWorkspaces\*\Data\msmdsrv.port.txt"
netstatAnyFilter netstat -ano LISTENING rows by (Get-Process msmdsrv).Id

Watch (verified July 2026, preview): The Power BI Desktop Bridge is a preview IPC surface hosted inside the Desktop process — a named pipe pbi-desktop-bridge-<processId> speaking JSON-RPC 2.0 (Content-Length framing), enabled by default under Options → Preview Features → "Enable external tool access to Power BI Desktop through secure local APIs". Discover methods with bridge.manifest (call it first; the surface changes per Desktop version, and unsupported methods return -32601 MethodNotFound). This could eventually supersede the msmdsrv port-discovery + AS-proxy dance in this skill — pipe name keys off the PID (no msmdsrv.port.txt / netstat needed), and one Desktop window = one bridge. Not there yet: it's preview with an unstable API, currently oriented toward report-authoring / agentic scenarios, and does not replace TOM metadata + ADOMD DAX access covered here. Track the manifest and GA status before relying on it. Re-verify the Learn page before acting on this note.

Connection Strings

TargetConnection String
TOM metadataData Source=localhost:<PORT> via Microsoft.AnalysisServices.Tabular.Server.Connect()
ADOMD DAXData Source=localhost:<PORT> on AdomdConnection
$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"

$server = New-Object Microsoft.AnalysisServices.Tabular.Server
$server.Connect("Data Source=localhost:$port")
$db = $server.Databases[0]
$model = $db.Model

Always $server.Disconnect() when done. Only $model.SaveChanges() persists modifications; without it edits are discarded. Ctrl+Z in PBI Desktop cannot undo TOM saves.

Executing DAX via ADOMD.NET

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()

$cmd = $conn.CreateCommand()
$cmd.CommandText = "EVALUATE SUMMARIZECOLUMNS('Date'[Year], ""@Total"", [Total Sales])"
$reader = $cmd.ExecuteReader()
while ($reader.Read()) {
	for ($i = 0; $i -lt $reader.FieldCount; $i++) {
		"$($reader.GetName($i)) = $($reader.GetValue($i))"
	}
}
$reader.Close(); $conn.Close()

DAX rules over ADOMD:

  • Fully-qualify columns: 'Sales'[Amount], never [Amount]
  • Measures unqualified: [Total Revenue]
  • String literals: double quotes, escape "" inside PowerShell here-strings
  • Prefer SUMMARIZECOLUMNS over summarize; prefer DIVIDE; never IFERROR
  • Reader returns fully-qualified column names. Access by index via GetName($i) / GetValue($i) — short-name indexing silently returns blanks
  • Use ExecuteNonQuery() for TMSL commands; ExecuteReader() only for EVALUATE

Validating DAX Before Saving

$testExpr = "SUM('Sales'[Amount]) / COUNTROWS('Sales')"
$cmd.CommandText = "EVALUATE ROW(""@Test"", $testExpr)"
try { $cmd.ExecuteReader().Close(); "VALID" } catch { "INVALID: $($_.Exception.Message)" }

For table expressions wrap in COUNTROWS(). For RLS filter expressions wrap in CALCULATETABLE(ROW("@OK",1), <filter>).

Refreshing the Model

Three equivalent methods:

MethodCallWhen
TMSL via TOM$server.Execute($tmsl)Full flexibility; error rowset
TOM API$model.Tables["X"].RequestRefresh([...]); $model.SaveChanges()Simplest programmatic
TMSL via ADOMD$cmd.ExecuteNonQuery() with TMSLWhen only ADOMD is loaded
Refresh TypeTMSLEffect
full"full"Drop, re-query, recompress, recalc DAX
calculate"calculate"Recalc DAX only (no source hit)
automatic"automatic"Per-partition: full if never processed else calculate
dataOnly"dataOnly"Re-query, skip DAX recalc
clearValues"clearValues"Drop data without reloading
defragment"defragment"Recompress dictionaries

Refresh dependencies: dimensions first, facts second, then a calculate to recompute DAX. Or issue full on the database and let the engine order it. Refreshes are synchronous — bump shell timeouts to 300000ms+ for large models.

VertiPaq / Storage Engine DMVs

All run via ADOMD $conn.

DMVUse
$SYSTEM.DISCOVER_STORAGE_TABLE_COLUMN_SEGMENTSPer-column USED_SIZE, DICTIONARY_SIZE, RECORDS_COUNT, SEGMENT_NUMBER
$SYSTEM.DISCOVER_STORAGE_TABLE_COLUMNSPer-column CARDINALITY (distinct values)
$SYSTEM.DISCOVER_SESSIONSLast command + SESSION_LAST_COMMAND_ELAPSED_TIME_MS / CPU_TIME_MS
$SYSTEM.DISCOVER_COMMANDSActive commands (longer COMMAND_TEXT than sessions)
$SYSTEM.TMSCHEMA_TABLES / _MEASURES / _COLUMNS / _RELATIONSHIPSModel metadata via SQL-like syntax

Filter out internal tables with WHERE LEFT(DIMENSION_NAME,1) <> '$'. High cardinality = large dictionary = memory hotspot.

SELECT DIMENSION_NAME, SUM(USED_SIZE + DICTIONARY_SIZE) AS TotalBytes, SUM(RECORDS_COUNT) AS Rows
FROM $SYSTEM.DISCOVER_STORAGE_TABLE_COLUMN_SEGMENTS
WHERE LEFT(DIMENSION_NAME,1) <> '$'
GROUP BY DIMENSION_NAME
ORDER BY TotalBytes DESC

Query Listener (Capturing Visual DAX)

Poll $SYSTEM.DISCOVER_SESSIONS once per second. When SESSION_LAST_COMMAND changes and starts with DEFINE|EVALUATE|VAR , record it. Timings come from SESSION_LAST_COMMAND_ELAPSED_TIME_MS and SESSION_CPU_TIME_MS.

Ignore internal traffic: <Subscribe>, <ImageSave>, <Batch><Discover>, MDSCHEMA_*, SELECT * FROM $SYSTEM.*.

Visual query anatomy:

PatternMeaning
SUMMARIZECOLUMNSMain group-by + measures
__DS0FilterTableSlicers/page filters
TOPN wrapping SUMMARIZECOLUMNSVisual has Top N filter
IGNORE(...)Column in group-by but not filter
__ValueFilterDM1Measure-based visual filter
CALCULATE(...) around a measureVisual-level override

Limits: SESSION_LAST_COMMAND is truncated for very large queries; intermediate queries between polls are missed (use 200-500ms interval for busy reports); thin-report queries are invisible.

EVALUATEANDLOG Debugging

EVALUATEANDLOG(<Value>, [<Label>], [<MaxRows>]) wraps a DAX expression, returns it unchanged, and emits intermediate results as a JSON trace event (DAXEvaluationLog, event class ID 135). PBI Desktop only; passthrough in Service / SSAS / Azure AS.

Setup via TOM Trace API: dot-source scripts/setup-evaluateandlog-trace.ps1 — assumes a connected $server (see "Discovering Running PBID Instances" above) and leaves $trace, $evalEvents, and $job in scope for the caller. The script body is the synchronized-ArrayList pattern; the rationale, cache-clear requirement, and FE/SE timing quirks are below.

Critical: Register-ObjectEvent -Action runs in a separate runspace; $global: does not cross. Use a synchronized ArrayList via -MessageData.

Always clear the VertiPaq cache before each debug query; cached results skip EVALUATEANDLOG:

$server.Execute('{ "clearCache": { "object": { "database": "' + $db.Name + '" } } }') | Out-Null

Events arrive asynchronously. Use a persistent trace across queries, record $evalEvents.Count before each call, and Start-Sleep -Seconds 2 before reading. Some calls emit no event due to engine optimizations. COUNTROWS(EVALUATEANDLOG(table)) is a known broken pattern.

JSON payload fields:

FieldMeaning
expressionWrapped expression text
labelOptional label argument
inputsColumns affecting evaluation context
data[].input / .outputPer-context input/output pairs
data[].rowCountTrue row count (not truncated by MaxRows)

Cleanup: $trace.Stop(); Unregister-Event $job.Name; $trace.Drop().

Performance Profiling (FE vs SE Split)

Equivalent to DAX Studio's Server Timings. Subscribe to QueryEnd, VertiPaqSEQueryEnd, VertiPaqSEQueryCacheMatch.

EventUseColumns
QueryEndTotal query timeTextData, Duration, CpuTime, EventClass
VertiPaqSEQueryEndPer-scan SE durationTextData, Duration, CpuTime, EventClass
VertiPaqSEQueryCacheMatchCache hitsTextData + EventClass only — adding Duration/CpuTime crashes $trace.Update()

Formula Engine time = QueryEnd.Duration - Σ VertiPaqSEQueryEnd.Duration.

Take 6-12 samples, compare medians not means, discard the first cold-cache run as warm-up. If ranges overlap significantly, the delta is noise.

DAXLib Package Patterns

daxlib.org is a registry of DAX UDF (user-defined function) packages. Requires compatibility level 1702+ — installing upgrades CL automatically and is irreversible; confirm with user before running on models below 1702.

ConceptNotes
Function formatfunction 'PackageId.FunctionName' = ( p1: STRING, p2: NUMERIC VAL, e: SCALAR EXPR ) => ...
Parameter modesVAL (eager, pre-evaluated); EXPR (lazy, evaluated in callee context). EXPR required for measure refs
Parameter typesSTRING, INT64, NUMERIC VAL, SCALAR EXPR, ANYVAL, ANYREF EXPR, COLUMN
Tracking annotationsDAXLIB_PackageId, DAXLIB_PackageVersion on every function
Call syntax[PackageId.FunctionName](arg1, arg2, ...) — bracket notation like a measure
Package trackingScan $model.UserDefinedFunctions by annotation to list/update/remove

CLI (daxlib): search | info | versions | functions | download (standalone); add | update | remove | installed (require --port <PORT> and .NET 8 SDK). Updates replace all functions matched by DAXLIB_PackageId annotation; user-created functions are untouched.

SVG measures from DaxLib.SVG return data:image/svg+xml;utf8,.... Set the column's data category to "Image URL" so Power BI renders inline in Table/Matrix/Card/Multi-row Card.

Calendar Column Groups (Compatibility Level 1604+)

Declarative date hierarchy + time intelligence mapping. A Calendar lives on a date table and contains one or more CalendarColumnGroup entries, each mapping a column to a TimeUnit enum. With calendars defined, DATESYTD / DATESMTD / SAMEPERIODLASTYEAR / DATEADD automatically adapt to the declared hierarchy.

Time UnitPurposeComplete/Partial
YearComplete yearcomplete
QuarterIncludes year context (Q3 2024)complete
QuarterOfYearPosition 1-4partial
MonthIncludes year (January 2024)complete
MonthOfYearName/number without yearpartial
WeekIncludes year (2024-W49)complete
WeekOfYearWeek number onlypartial
DateSpecific datecomplete
DayOfMonth / DayOfWeek / DayOfYearDay positionspartial

Rules:

  • Calendar names must be unique across the entire model, not just the table
  • Each calendar draws columns from one host table only
  • Never repeat a time unit within the same calendar
  • A column mapped to a unit in one calendar must map to the same unit in every other calendar it participates in
  • Each column group has one primary column (sort key) + optional associated columns (display labels). If Year Month is sorted by Year Month Number, the Number column is primary
  • Only one TimeRelatedGroup per calendar — combine IsWeekend, Season, RelativeMonth, etc. into it
  • Week-based (ISO / 4-4-5) calendars: always pair ISO weeks with ISO year; map Period to the Month time unit
$cal = New-Object Microsoft.AnalysisServices.Tabular.Calendar
$cal.Name = "Gregorian Calendar"
$g = New-Object Microsoft.AnalysisServices.Tabular.CalendarColumnGroup
$g.TimeUnit = [Microsoft.AnalysisServices.Tabular.TimeUnit]::Month
$g.PrimaryColumn = $dateTable.Columns["Year Month Number"]
$g.AssociatedColumns.Add($dateTable.Columns["Year Month"])
$cal.CalendarColumnGroups.Add($g)
$dateTable.Calendars.Add($cal)
$model.SaveChanges()

Transactions and Rollback

SaveChanges() is a single atomic commit — any validation failure rolls back the whole batch. For pre-commit inspection, use $model.UndoLocalChanges() to discard pending modifications since the last save. There is no explicit begin/commit on the local AS proxy.

Finding the Source File Path

TOM does not expose .pbix/.pbip path. Most reliable source is PBI Desktop's User.zipSettings.xmlFileHistory entries (ordered by lastAccessedDate). The first entry is usually (but not guaranteed to be) the currently-open file. Confirm with user when ambiguous.

Fallbacks: non-Store install exposes path in Get-Process PBIDesktop | Select MainWindowTitle. Win32_Process msmdsrv.exe command line reveals the workspace path but not the source file.

Reference

Gotchas

IssueCauseFix
TOM connect failsThin report connected to remote model; Direct Lake; no model loadedUse remote XMLA / Power BI REST; open a real model
Multiple msmdsrv processesMultiple PBI Desktop windows openEnumerate all ports, read Databases[0].Name, ask user
Short-name reader returns blankADOMD returns qualified column namesIterate by index; use GetName($i)
Register-ObjectEvent sees empty eventsAction block runs in isolated runspacePass synchronized ArrayList via -MessageData
EVALUATEANDLOG silentCached results; engine optimization; known COUNTROWS bugClear cache; restructure wrapper; some calls emit nothing
VertiPaqSEQueryCacheMatch crashes traceDuration/CpuTime columns unsupportedAdd only TextData + EventClass
Bash eats $env:TEMP / $serverBash interpolation before PowerShell sees itSingle-quote -Command or use .ps1 with here-string
TmdlSerializer type missingStale .retail.amd64 packageUpgrade NuGet package or use Fabric REST definition APIs
daxlib upgrade irreversibleCL 1702 forced on installConfirm with user first; older tools won't open the model afterward
Refresh exceeds bash timeoutDefault 120000ms too lowSet timeout: 300000 or higher
Save appears lostForgot $model.SaveChanges()Always save; no Ctrl+Z undo afterward
Orphaned msmdsrv processForgot $server.Disconnect()Always disconnect in finally/cleanup
Calendar collisionSame time unit twice, or name not globally uniqueOne time unit per calendar; globally unique names

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.