← Back to case studies

Case Study 05

What happened when I started treating Power BI reports like code

How PBIP, Git and a small validation toolkit let me use AI agents to change reports quickly, without giving up diffs, validation, or an easy rollback path.

2026 · 8 min read
  • Power BI
  • PBIP
  • AI agents
  • Git
  • CI/CD
  • Python

Summary

I started building Power BI reports with AI agents working directly on the project files. It made me much faster. It also forced me to answer a question I didn't have before:

How much autonomy can I give an AI agent before I need stronger engineering controls?

This case is about how my way of working changed: what development gained by going through the agent, what new kind of risk showed up, and which controls I had to add as the agent made bigger changes.

The shift: from a GUI to a project

For years a Power BI report was a .pbix file: a binary edited by clicking in Power BI Desktop. With the project format (PBIP), the model becomes TMDL and the report becomes PBIR: folders of plain-text files you can read, diff and version.

That changed more than the format. An AI agent that works with files can now add a visual, change a filter, reorganize a page or refactor measures. The unit of work stopped being a GUI and became a project.

Before
  1. Idea
  2. Power BI Desktop
  3. Manual editing
  4. .pbix file
  5. Publish
The report is a file you edit by clicking.
Now
  1. Idea
  2. AI agent
  3. PBIP files (plain text)
  4. Git diff
  5. Validation
  6. Review
  7. Publish
The report becomes a project of version-controlled files that an agent can edit as code.

There are tools that give an agent ways to interact with Power BI, and they're useful. For the way I work, something else mattered more: a versioned artifact the agent edits like code. The two ideas complement each other.

The problem: faster at everything, including breaking things

If an agent can change a whole report in seconds, it can break it in seconds too. And the dangerous mistakes aren't the ones that stop the project from opening: they're the ones that leave a number that looks fine and is wrong.

A concrete example. The agent changes a card's filter so it covers two funnel stages:

  "filter": { "In": {
    "Expressions": [ Stages[Order] ],
-   "Values": [ [ 3 ] ]
+   "Values": [ [ 2 ], [ 3 ] ]
  } }

And the measure that card shows, in another folder of the project, resolves the row with MAX:

Deals at stage =
    VAR _order = MAX ( Stages[Order] )
    RETURN
        CALCULATE ( COUNTROWS ( Deals ), Stages[Order] = _order )
Sales.Report / visual.json
PBIR · the visual
Stages[Order] IN (2, 3)
valid on its own
Sales.SemanticModel / Deals.tmdl
TMDL · the measure
MAX ( Stages[Order] )
valid on its own
  1. The visual's filter admits stages 2 and 3
  2. The measure uses MAX(Stages[Order])
  3. It ends up resolving stage 3 only
  4. The value shown may not represent the context the visual appears to have
Each file is valid on its own. The defect appears when you combine them: a cross-file coherence problem.

Each piece is valid on its own. Together, the filter admits stages 2 and 3, but the measure uses MAX and ends up resolving stage 3 only: the value shown may not represent the context the visual appears to have. Nothing is malformed, so no structural validator has any reason to flag it.

This isn't a lab case. In a real report, a visual produced an impossible conversion rate above 100%. The underlying measure was valid; the problem was the filter context passed by the visual. The example above is its synthetic equivalent, and anyone can reproduce it from the repo.

The filter lives in the report and the measure in the model. Nothing was checking the relationship between the two.

Valid is not the same as correct

What sorted it out for me was separating three different questions, which need three different kinds of check. The first one already had tools. For the second I couldn't find any, and that's where pbip-guardrails ended up coming from.

  1. Level 1 · Valid
    Can Power BI parse it?
    Microsoft validators (TMDL and PBIR)
    The official TMDL parser and the PBIR CLI. They catch a model that wouldn't even open in Desktop, with file and line.
  2. Level 2 · Coherent
    Are the pieces of the report coherent with each other?
    pbip-guardrails
    Reads each measure's DAX together with the filters of the visual that uses it: missing measures, incompatible filters, mixed populations, hidden visuals.
  3. Level 3 · Correct
    Does the number represent what the business thinks?
    People + reconciliation with the source data
    No tool answers it. It takes business judgement and a comparison against the source.
They don't compete: each level answers a question the previous one can't.

The guardrails around the agent

With that clear, the workflow ended up like this. It's not an end-to-end automated pipeline: some steps are done by a tool and others are decided by a person, and the diagram tells them apart.

  1. Natural-language request
  2. AI agent
  3. Edits the PBIP files
  4. Review the Git diff
  5. pbip lint
  6. Level 1 · Valid
    TMDL + PBIR
    Microsoft validators
    ✓ pass
  7. Level 2 · Coherent
    visual ↔ measure
    pbip-guardrails
    ✓ pass
    Failure path · example
    Inspect the diff · fix · roll back
    ↺ back to the agent
  8. safe-copy → ZZ- report
  9. Publish the copy to Power BI Service
  10. Refresh and visual review
  11. Publish the real report
Not prompt → production.
  1. Prompt
  2. Artifact
  3. Evidence
  4. Human-controlled release
done by a tool decided by a person
  1. A known Git state. I didn't start with this process: it emerged as the agent made bigger changes. Now, before letting an agent make substantial changes, I want the project in a known Git state. That gives me a readable diff and a cheap rollback path.
  2. pbip lint. Runs Microsoft's validators and the coherence checks. If something fails, the change goes back to the agent with the finding, or gets rolled back.
  3. safe-copy. The report is first published as a copy under another name (ZZ-…). Since publishing matches items by name, the copy is created as a new item and never overwrites the real one.
  4. Look at it. There's no Power BI renderer for Linux: the only proof a visual looks right is opening it. Only then is the real one published.

What each tool catches

I built a synthetic report with defects planted on purpose and ran it through every layer. The point isn't to show that one tool "wins": different tools answer different questions.

Defect Structural validators Coherence checks
A measure that no longer exists — ✓
Multi-value filter + a measure using MAX — ✓
A visual crossing metrics of two populations — ✓
A duplicate visual hidden under another — ✓
A visual outside the canvas ✓ (warning) ✓
TMDL that can't be parsed ✓ (any error) one known pattern only

The last two rows show how they complement each other. The PBIR validator flags the visual outside the canvas; the TMDL parser catches any syntax error, while my check only recognizes one specific pattern that had once broken a whole project. This is what the finding from the example above looks like:

$ pbip lint examples/broken --family 'Segment (\d+)'
  'Pipeline' [filters] cardVisual b0000000000000000001: Order IN ['2L', '3L']
      -> ['Deals at stage'] depend(s) on the row's Order: the value shown may not match the filter

I didn't trust the AI

An agent wrote a good part of pbip-guardrails's code, quickly. That leaves the question any engineer would ask: how do I know it didn't invent a solution that only looks like it works? That's why nothing was accepted on its own: every step was checked against something independent.

  1. AI-generated code
  2. Known fixture
  3. Tests
  4. Official tools
  5. Parity with the previous version
  6. Real-world read-only validation
  1. A case with known answers. A script generates a healthy report and a broken one, with a defect planted for every kind of check.
  2. Automated tests. The healthy one must give zero findings; the broken one, exactly the ones planted.
  3. The official tools as a reference. Microsoft's parser and CLI run on the same case: the healthy one passes, the broken TMDL fails where it should.
  4. Parity with the previous version. The checks started as an internal script. The generalized version had to return exactly the same findings on several real PBIP projects, read-only.
  5. Fixes that came from looking, not assuming. The first "healthy" report the agent generated had a real TMDL error, and the linter itself caught it. I kept that as a lesson: the fixture gets validated too.

Limits

  • Passing the linter doesn't guarantee the report renders. A query can fail under some filter, or a permission can be missing. That's why the ZZ- copy exists.
  • Level 3 is still human. No tool knows what "active customer" means in your business.
  • The checks are heuristic and tuned not to be noisy. Each rule discounts the known legitimate cases; if you get hundreds of findings, the threshold is wrong, not the report.

What I learned

The format enables everything

Diff, rollback, CI, linting and review exist because PBIP turned a GUI artifact into one an agent can edit.

Speed calls for controls

If I let an AI touch my reports, I need the same controls we already take for granted when an AI touches code.

Name the level

"Is it valid?", "is it coherent?" and "is it correct?" are different questions. Mixing them up is how a mistake gets through.

The code

pbip-guardrails is open source (MIT): the coherence checks, the integration with Microsoft's validators, the safe copy for publishing, a synthetic sample to try it on, and a CI example.

View pbip-guardrails on GitHub →

Tap outside the image to close