Power BI

Reviewing 200 DAX measures with Claude

What an AI agent finds in a bloated model, what it misses, and why human review still matters.

A Power BI model that has been in production for a few years tends to end up in the same place: 200-plus measures, built by several people who have since moved on, and nobody left who can say with confidence which measure to trust for something as basic as revenue. Every new request just adds one more measure to the pile, because writing a new one is faster than understanding the old ones. It’s one of the most familiar patterns in legacy BI.

This is how I review a model like that, what an AI agent reliably surfaces, and where it still needs me. When I take one on, I stop adding measures and start with a read-only review: Claude Code connected to the local Power BI MCP server, currently in preview. I use it to inventory model objects, inspect measure definitions and dependencies, and run targeted DAX validation queries.

How I run a review like this

The point isn’t to let the agent rewrite the model. It’s to let it read the model at a scale I can’t, and hand me a map I can act on.

  1. Inventory every measure, column, relationship and dependency through the MCP.
  2. Cluster the measures by normalized DAX patterns, naming similarities and dependency signals.
  3. Classify each finding as a duplicate candidate, a naming issue, a documentation gap, or logic that needs review. Nothing is “redundant” yet, only a candidate.
  4. Validate every proposed change with targeted DAX queries, report and downstream-consumer checks where available, and business context before applying it.

A human reviewer usually works through measures one at a time and gradually loses the full picture. An agent can inventory, retrieve and compare across the whole model in a far more systematic pass. That difference is the whole reason to use one.

What it surfaces

A first cluster of candidates often looks like this:

Total Sales      = SUMX(Sales, Sales[Qty] * Sales[Price])
Revenue          = SUMX(Sales, Sales[Qty] * Sales[Price])
Sales Amount LY  = SUMX(Sales, Sales[Qty] * Sales[Price])  -- not last year at all

Three names, one calculation, and a third measure whose name promises a prior-year figure it never computes. The agent proposes a single canonical Total Sales, a rename map for the references that point at the duplicates, and flags the mislabelled one for review.

A model like this usually hides ten or more clusters of the same kind, and not all of them are real duplicates. Some measures differ only by a FORMAT call. Those are not immediate merge candidates: FORMAT can change the return type from a number to text, alter sorting behaviour, or break a visual or downstream calculation that expects a number.

Others are wrapped in a CALCULATE that appears redundant and needs validation before simplification. By the end, a model that advertises 200 ideas can reveal far fewer distinct calculation patterns, wearing many more names than it needs.

Names can mislead. Display folders can mislead. DAX gets you closer to the truth, but business intent still lives outside the formula.

Where it gets things wrong

This is the part that matters, because an agent that never failed would just be a sales pitch.

It will flag measures as duplicates that are not. Two measures can look identical until you notice a single KEEPFILTERS:

Sales Bikes =
CALCULATE([Total Sales], 'Product'[Category] = "Bikes")

Sales Bikes (Keep Filters) =
CALCULATE([Total Sales], KEEPFILTERS('Product'[Category] = "Bikes"))

The first replaces an existing filter on Product[Category]. The second intersects with it. In a report already filtered to Accessories, one can still return Bikes sales while the other returns blank. Similar DAX, different business behaviour.

The agent sees two near-identical expressions. Only the report context, business intent and downstream impact tell you whether they have to stay separate. Merge them on the agent’s word and a report someone relies on can start returning different numbers quietly.

That is why every finding is a candidate, not a verdict. The agent reads faster than any human and never gets bored on measure 174. But it doesn’t know which of two near-identical measures the business depends on, or that the “redundant” one is load-bearing in a single report nobody told it about.

I delegated the reading, not the decision

That distinction is the whole method. I didn’t delegate the decision. I delegated the reading. The agent gave me a map I could never have built by hand in the time I had: every duplicate candidate, every measure with no description, every name that promises something the DAX doesn’t deliver. Then I sat with that map and decided, measure by measure, what to keep, what to merge and what to leave alone because it was quietly important.

The audit was fast because of the agent and correct because of the review.

Before you connect a model to an agent

One caution matters more in consulting than almost anywhere else. Depending on the MCP client and LLM provider, metadata, schemas and query results exposed through tools can be processed outside Fabric’s compliance boundary.

Before connecting a client model to an agent, confirm the approved client, model provider, tenant configuration, permissions, access scope and data-handling terms. Start with read-only analysis, work from a backup or PBIP branch, and remember that the server is still in preview.

The agent didn’t decide what the model should become. It made the model readable enough for me to decide.

References