• Power BI
  • DAX UDFs
  • M365 Copilot
  • Semantic Model

Copilot-Ready Power BI: Why DAX UDFs Now Matter

M365 Copilot now writes DAX against your semantic model. Table-valued DAX UDFs, documented in AI Instructions, are what keep its answers consistent.

6 min read

Contents5 sections
  1. When Copilot stops drawing charts and starts writing queries
  2. The semantic model becomes the contract
  3. A table-valued UDF, in finance terms
  4. Where the logic actually has to live
  5. What this means for finance and BI teams

Two analysts ask Copilot the same question against the same semantic model and get two different numbers. Neither is exactly wrong. One phrased the request a little differently, Copilot wrote a slightly different DAX query, and now two versions of "revenue by segment, last quarter" are circulating in email threads. That quiet problem rides along with the new direction Microsoft just announced for Power BI Copilot, and it lands as a governance issue before it becomes a technical one.

The core of the change: M365 Copilot, rather than the in-product Power BI Copilot, now answers questions by generating and running DAX queries directly against your semantic model. It needs an M365 Copilot licence, but no Fabric capacity: it works with models in Pro workspaces too. The preparation you already put into making your models Copilot-friendly is not wasted. What changes is where the fragile part sits.

When Copilot stops drawing charts and starts writing queries

The older Power BI Copilot could answer in several shapes, including by constructing a visual for you. M365 Copilot mostly does one thing instead: it writes DAX and runs it. It is genuinely good at producing syntactically correct DAX, and it respects your model's AI Instructions. Two risks survive that competence, and both will be familiar to anyone who has run a reporting function.

The first is cost and performance. A query can return the correct result while being nowhere near optimal, which shows up on your capacity bill as consumed CUs and on your users' screens as a slow answer. The second is consistency. When several people want the same analysis but phrase it vaguely or differently, each can get a slightly different result. In a finance function, that kind of variation between users is exactly what month-end close and reconciliation are meant to remove.

The semantic model becomes the contract

Logic that used to live inside a specific report visual, or inside an analyst's head, now has to live somewhere Copilot will reach for every time. Free-text guidance in AI Instructions helps, but a prose description of how an analysis should be done is a weak contract.

The model can follow that prose, ignore it, or improvise around it.

A table-valued DAX User-Defined Function is a much stronger contract, because it is the computation rather than a description of it. The analytical operations finance teams least want re-derived on the fly are exactly the ones worth encapsulating this way: aging buckets, Pareto or ABC segmentation of customers and spend, materiality banding of variances, allocation keys.

For years I built DAX-heavy semantic models in capital markets, including an activity-based expense allocation and a global expense dashboard used by more than fifty people at Morgan Stanley. With that many hands on the same numbers, the risk is rarely one broken figure; it is capable people computing "the same" number in slightly different ways, because the definition lives in shared convention rather than in the model itself. Conventions drift, people leave, and the query saved in someone's local PBIX quietly diverges from the one in the reporting pack. A UDF moves that definition into a single place every consumer, human or agent, has to pass through.

A table-valued UDF, in finance terms

The pattern is small. You define a function once, it returns a table, and any DAX query can call it with EVALUATE. Here is the shape, generalised to an accounts-receivable aging that buckets open items by a cut-off date and two day-count thresholds:

DAX
-- Reusable, table-valued logic centralised in the semantic model.
-- Buckets open receivables into aging tiers as of a cut-off date.
EVALUATE
    AgingBuckets( DATE(2026, 6, 30), 30, 90 )
ORDER BY [Aging Bucket]

The function decides how items are classified, in one audited place. Copilot's job shrinks to calling it with the right arguments and returning what comes back, untouched. To make it do that reliably, you describe the function in the model's AI Instructions, roughly like this:

Text
This model contains a table-valued function, AgingBuckets(asOfDate, tier1Days, tier2Days),
that classifies open receivables into aging tiers and returns a table for EVALUATE.
When a user asks for AR aging, always call this function and return its table unchanged:
do not add columns, totals, or unrelated measures.
If the user has not supplied the cut-off date or the day thresholds, ask for them.
Never assume default values.

One caveat worth knowing up front: Power BI reports still cannot display the output of a table-valued UDF directly. That limitation used to make the pattern feel academic, but it matters far less once your main consumer is a query tool like Copilot that can run any DAX it likes.

Where the logic actually has to live

Here is the detail that surprised me in the source material. The same description worked when it sat in the model's AI Instructions, but was ignored when the author moved it into the Description property of the UDF itself. Copilot went off and wrote its own query, which returned the right answer while dragging in extra measures nobody had asked for. Telling Copilot which functions exist and when to use them has become a core modeling task, on the same footing as the model itself.

Where the analysis logic livesCopilot's behaviourRisk
Nowhere reusable; Copilot free to write any DAXGenerates its own query each timeSuboptimal performance and CU cost; inconsistent results between users
In the UDF, described only in its Description propertyIgnored the UDF and wrote its own DAXCorrect number, but stray measures and no consistency guarantee
In the UDF, described in the model's AI InstructionsCalled the UDF and returned its table unchangedConsistent output, and you control the query that runs

What this means for finance and BI teams

The instinct when a new AI feature ships is to test the AI. The more useful move is to invest in the layer beneath it. A clean semantic model, with business logic captured in UDFs, documented where the agent will read it, and protected by the row-level security you already maintain, is what turns a confident-sounding Copilot answer into a trustworthy one. Copilot inherits your model's security and definitions; it does not fix a model that was ambiguous to begin with.

For a controller, the checklist is unglamorous and short. Move the analytical operations you care about into reusable functions. Describe them in AI Instructions, including the rule that Copilot must ask for missing parameters rather than guess. Keep RLS and governance current, because an agent that can write any query is only as safe as the model it queries. This is close to the governance work I do on Fabric lakehouses now, and the semantic layer is where most of the payoff hides.

Chris Webb walks through the mechanics, with a worked ABC-analysis example and the AI Instructions that made it stick, in his post Power BI, M365 Copilot And The Importance Of DAX UDFs. The feature is still evolving, with more capabilities promised and rough edges to smooth. The unglamorous part, the reusable logic and the clean model, is what will still matter after the demos change.