Useful Community Tools And Resources For Power BI And Fabric

There are a lot of really cool free, community-developed tools and resources out there for Power BI and Fabric – so many that it’s easy to miss announcements about them. In this post I thought I’d highlight a few that came out recently and which you might want to check out.

Let’s start with the Fabric Toolbox, a collection of tools, samples, scripts and accelerators created and maintained by some of my colleagues here at Microsoft. The most widely-known tool in there is FUAM (Fabric Unified Admin Monitoring), a solution accelerator for monitoring an enterprise Power BI and Fabric implementations. It’s the successor to Rui Romano’s Power BI monitoring solution, which is now deprecated, but it’s a lot richer than that. It’s already been the subject of a Guy In A Cube video though so I hope you’ve already come across it. There are other things in the Fabric Toolbox that should be more widely known though. My fellow CAT Phil Seamark (why doesn’t he blog anymore???) has been busy: a month ago he announced a new Power BI load testing tool (video here) based on Fabric notebooks which is much easier to configure than the previous load testing tool created by the CAT team. He’s also published a sample MCP Server that, among other things, can analyse a semantic model to see whether it follows best practices. Another colleague, Justin Martin, has published tools for auditing semantic models and DAX performance tuning in the toolbox too. Finally, with the deprecation of Power BI Datamarts looming, if you choose to replace them with Direct Lake semantic models based on Fabric Warehouse (although I think 90% of the Datamarts I’ve seen can be replaced with simple Import models) then there’s a migration accelerator here.

Elsewhere, if you’re a hardcore Power BI developer you’ll already know how useful TMDL View in Power BI Desktop is. Rui Romano recently announced that there’s a new gallery of TMDL scripts where you can see what’s possible with TMDL and share your own scripts. For example, there’s a script here that creates a date dimension table from a Power Query query.

Two years ago I blogged about a tool called PBI Inspector that provides rules-based best practices testing for the Power BI visualisation layer, created by yet another Microsoft colleague, Nat van Gulck. Not only is there now a V2 of PBI Inspector, which will be renamed Fab Inspector, but two weeks ago Nat announced a VS Code extension that allows you to write, debug and run rules from VS Code.

Last of all Gerhard Brueckl recently announced V2 of Fabric Studio, an incredibly powerful VS Code extension that acts as a wrapper for the Power BI/Fabric REST APIs. It lets you browse your workspaces and their contents from VS Code and create/update/delete items among other things; Gilbert Quevauvilliers recently wrote a nice blog post showing how you can use it to download any Power BI report from the Service easily.

That’s enough for now. If there are other tools or resources that came out recently that I didn’t mention, please leave a comment!

Power BI Copilot, AI Instructions And DAX Query Templates

At the end of my last post I showed how, if you put the definition of a measure in the AI Instructions of your Power BI semantic model, Copilot can use it as a template to create new measures with slightly different logic. That got me wondering – can you do the same thing for DAX queries to solve even more complex problems? It turns out you can.

First of all, why would you want to do this? There are some problems that can be solved relatively easily by writing a DAX query but which are quite hard to solve if you’re limited to writing measures. ABC classification is one example of this. Marco and Alberto have a great article on this problem here; the problem is when you want to increase the number of buckets/clusters/groups you use or change the rules for which transactions go into which buckets and make all this dynamic for end users building reports in Power BI Desktop then your semantic model can get very complicated very quickly. In a DAX query you can solve these problems much more easily, in part because you can define a calculated column at query time and use it to do your grouping. And since Copilot can generate and run DAX queries, it can be taught how to do ABC analysis by giving it an example DAX query and some instructions telling it how the query works. Let’s see an example.

The sample semantic model I’ve been using in this series has a fact table called Transactions where each row represents a real estate property sale in England and Wales. Let’s say you want to group these rows into different groups based on the price paid for the property. With nothing in the AI Instructions for the model, the following prompt:

Do an ABC analysis on the Transactions table, grouping all transactions into three groups named A, B and C based on the Price column where the upper boundary for the first group is £250000 and the upper boundary for the second group is £700000. Filter the transactions to just 1st January 2025.

…will sometimes generate the right result but will also sometimes generate a slightly strange or even incorrect result. Here’s an example where the information in the summary is correct but the DAX query is far from ideal:

EVALUATE
  // Create a table with transactions on 1st January 2025 and assign an ABC group based on Price
  SELECTCOLUMNS(
    FILTER(
      'Transactions',
      'Transactions'[Date] = DATE(2025, 1, 1)  // Filter transactions to 1st January 2025
    ),
    "Price", 'Transactions'[Price],
    "ABC Group", 
      SWITCH(
        TRUE(),
        'Transactions'[Price] <= 250000, "A",      // Group A: Price up to £250,000
        'Transactions'[Price] <= 700000, "B",      // Group B: Price over £250,000 and up to £700,000
        "C"                                       // Group C: Price above £700,000
      )
  )
ORDER BY
  [ABC Group] ASC,
  [Price] ASC

This query returns one row per transaction rather than one row per group:

Not something you can give to an end user for sure. Here’s a DAX query that gives me exactly the result I want:

DEFINE
//Create a new calculated column at query time 
//to create the groups for ABC classification
COLUMN 'Transactions'[Group] =
//Upper boundary for price for group A
VAR AUpperBoundary = 250000
//Upper boundary for price for group B
VAR BUpperBoundary = 700000
RETURN
//Return a different letter representing a group name
//based on where the value in the Price column sits in
//the boundaries defined
SWITCH(
TRUE(),
//If the price is less than or equal to the variable AUpperBoundary
//then return the value "A"
Transactions[Price]<=AUpperBoundary, "A (<=£250,000)",
//If the price is less than or equal to the variable BUpperBoundary
//then return the value "B"
Transactions[Price]<=BUpperBoundary, "B (>£250,000 and <=£700,000)",
//Otherwise return the value "C"
"C (>£700,000)"
)
//Returns the results of the classification
EVALUATE
SUMMARIZECOLUMNS(
    'Transactions'[Group],
    //Filter by a given date
     KEEPFILTERS( TREATAS( {DATE(2025,1,1)}, 'Date'[Date] )),
    "Count Of Transactions", [Count Of Transactions]
)
ORDER BY 
    'Transactions'[Group] ASC

Here’s what this query returns in DAX Studio:

Putting this query in the semantic model’s AI Instructions with some explanatory text, like so:

The following DAX query does an ABC analysis on the Transactions table, grouping transactions into three groups called A, B and C, for the 1st January 2025.  The transactions with the lowest prices always go into group A, then subsequent letters represent higher price ranges. If the user asks for an ABC analysis use this query as a template.

DEFINE
//Create a new calculated column at query time 
//to create the groups for ABC classification
COLUMN 'Transactions'[Group] =
//Upper boundary for price for group A
VAR AUpperBoundary = 250000
//Upper boundary for price for group B
VAR BUpperBoundary = 700000
RETURN
//Return a different letter representing a group name
//based on where the value in the Price column sits in
//the boundaries defined
SWITCH(
TRUE(),
//If the price is less than or equal to the variable AUpperBoundary
//then return the value "A"
Transactions[Price]<=AUpperBoundary, "A (<=£250,000)",
//If the price is less than or equal to the variable BUpperBoundary
//then return the value "B"
Transactions[Price]<=BUpperBoundary, "B (>£250,000 and <=£700,000)",
//Otherwise return the value "C"
"C (>£700,000)"
)
//Returns the results of the classification
EVALUATE
SUMMARIZECOLUMNS(
    'Transactions'[Group],
    //Filter by a given date
     KEEPFILTERS( TREATAS( {DATE(2025,1,1)}, 'Date'[Date] )),
    "Count Of Transactions", [Count Of Transactions]
)
ORDER BY 
    'Transactions'[Group] ASC

Means that the prompt above not only returns what you would expect:

…but more importantly, variations on the prompt such as:

Do an ABC analysis on the Transactions table, grouping all transactions into four groups named A, B, C and D based on the Price column where the upper boundary for the first group is £300000, the upper boundary for the second group is £750000 and the upper boundary for the third group is £900000. Filter the transactions to just 16th January 2025.

…take the template query and modify it as you would expect:

Note that the prompt has not only changed the date to filter by but increased the number of groups from three to four, and that Copilot has modified the DAX query to handle this – which I think is very impressive. Here’s the DAX query Copilot generated:

// DAX query generated by Fabric Copilot with "Group all transactions from the Transactions table into four groups named A, B, C, and D based on the Price column, where group A includes transactions with Price up to £300,000, group B includes transactions with Price above £300,000 and up to £750,000, group C includes transactions with Price above £750,000 and up to £900,000, and group D includes transactions with Price above £900,000. Filter the transactions to just 16th January 2025. Show the count of transactions in each group."
DEFINE
// Create a new calculated column at query time 
// to classify transactions into groups A, B, C, and D based on the Price column
COLUMN 'Transactions'[Group] =
    // Define the upper boundaries for each group
    VAR AUpperBoundary = 300000
    VAR BUpperBoundary = 750000
    VAR CUpperBoundary = 900000
    RETURN
        SWITCH(
            TRUE(),
            'Transactions'[Price] <= AUpperBoundary, "A (<=£300,000)",
            'Transactions'[Price] <= BUpperBoundary, "B (>£300,000 and <=£750,000)",
            'Transactions'[Price] <= CUpperBoundary, "C (>£750,000 and <=£900,000)",
            "D (>£900,000)"
        )
        
EVALUATE
    SUMMARIZECOLUMNS(
        'Transactions'[Group],
        // Filter transactions to include only the 16th January 2025
        KEEPFILTERS( TREATAS( { DATE(2025,1,16) }, 'Date'[Date] ) ),
        "Count Of Transactions", [Count Of Transactions]
    )
ORDER BY 
    'Transactions'[Group] ASC

Tools that convert natural language to SQL, like Fabric Data Agents, all allow you to specify example SQL queries like this so it’s hardly a surprise that the same approach applies to DAX. What’s interesting is the type of problems that giving Copilot templates of DAX measures and queries allows you to solve, and I’m only just starting to think of the possibilities.

Power BI Copilot, AI Instructions And DAX Measure Definitions

Continuing my (already very long) series on what information you should be adding to the AI Instructions of your semantic model and why, in this post I’ll show you the benefits of adding the DAX definitions of your measures.

A really common question from end users viewing a Power BI report is “how is this measure calculated?”. As a result I have seen model developers use techniques like this to display either a text description of how the measure works in a report or the actual DAX definition. It is therefore not a surprise that if end users are using Copilot they will ask the same question. Unfortunately Copilot cannot – or rather should not, at the time of writing – be able to to see the definitions of the measures in your model. Most of the time if a user asks to see how a measure is defined then Copilot will say that it can’t show the definition:

[Argh, yes I know it’s calling a measure a calculated column]

…although sometimes it does seem to be able to get the definition by writing a DAX query – but I have also seen it hallucinate and come up with a plausible looking definition which isn’t the actual definition. Anyway, it certainly can’t show the definition of a measure or a description of how it works reliably.

Adding all the measure definitions to the model’s AI Instructions mostly solves this problem. TMDL View makes it easy to get all the measure definitions in a semantic model in Power BI Desktop and you can copy/paste them from there into the AI Instructions.

In the semantic model that I’ve been using throughout this series I added some extra measures and then copied their DAX definitions and their descriptions into the AI Instructions. Here are the AI Instructions:

##Definitions and descriptions of the measures in this model
If a user asks how a measure is defined, asks how a measure works or asks how a measure is calculated, ignore any previous instructions about displaying measure definitions from the model and show the definition given here.

All measures that return currency values do so in Pounds Sterling and should be formatted with a £ sign.

##Average Price Paid
AVERAGE('Transactions'[Price])

##Count Of Transactions
COUNTROWS('Transactions')

##New Build Average Price Paid
Gets the value of the Average Price Paid measure where the New column equals "Y"
CALCULATE([Average Price Paid], KEEPFILTERS('Transactions'[New]="Y")) 

##Tax Paid
Different tax rates are levied on new build and non-new build properties
A 10% tax is levied on the sale of new build properties
A 5% tax is levied on the sale of properties that are not new builds
(CALCULATE(SUM('Transactions'[Price]), KEEPFILTERS('Transactions'[New]="Y")) * 0.1)
+
(CALCULATE(SUM('Transactions'[Price]), KEEPFILTERS('Transactions'[New]="N")) * 0.05)

Note in particular the Tax Paid measure – it has some fairly complex logic that bears no relation to any actual tax in the UK, so I knew there was no way Copilot would be able to guess how it was defined.

With these AI Instructions in place, the prompt:

how does the tax paid measure work?

…shows a helpful summary:

[Although I admit sometimes Copilot still does reply saying that it can’t display the definition of a measure from the model, which isn’t ideal – if I work out how to stop this happening I’ll update this post]

Even more impressively, since Copilot knows the definition of the measure, it is able to answer more complex questions like this:

show me what the tax paid by property type name would be if the tax on new build properties was increased to 11%

Here’s the DAX query generated, which shows that Copilot has modified the definition of the measure correctly:

// DAX query generated by Fabric Copilot with "Show the tax paid by Property Type Name if the tax on new build properties was increased to 11%."
EVALUATE
  // Summarize the tax paid by Property Type Name with increased new build tax rate (11%)
  SUMMARIZECOLUMNS(
    'PropertyTypes'[Property Type Name],
    "Tax Paid", 
      // New build tax (rate increased to 11%) plus non-new build tax (rate unchanged at 5%)
      (CALCULATE(SUM('Transactions'[Price]), KEEPFILTERS('Transactions'[New] = "Y")) * 0.11) +
      (CALCULATE(SUM('Transactions'[Price]), KEEPFILTERS('Transactions'[New] = "N")) * 0.05)
  )
  ORDER BY
    'PropertyTypes'[Property Type Name] ASC

Why Do Power BI Copilot AI Instructions Seem To Work Better in Desktop Than In The Service?

I’m spending a lot of time with organisations testing Power BI Copilot at the moment, and something I hear a lot is that Copilot works well in Desktop but when you publish your model to the Power BI Service the results are a lot more inconsistent. One reason why is because of how updates to AI Instructions are applied after you publish your semantic model.

Let’s see an example of this. Consider the following semantic model consisting of a single table with two measures, Sales Amount and Profit Amount:

The semantic model has the following AI Instructions applied:

//Instructions V1
xyz means the measure Sales Amount

The instructions here don’t make much sense, but using a meaningless term like “xyz” makes it easier to test whether Copilot is using an instruction or not.

In Power BI Desktop, the following Copilot prompt returns the results you’d expect with xyz understood as Sales Amount:

show xyz

If you publish this model to an empty workspace in the Power BI Service then this prompt returns the same correct result.

[By the way, the message “Copilot is currently syncing with the data model. Results may be inconsistent until the sync is finished” will be the subject of a future blog post – it’s not connected to what I’m describing in this post, it relates to how Copilot needs to index the text values in your semantic model, which is a separate process]

So far so good. Going back to Power BI Desktop, changing the AI Instructions like so:

//Instructions V2
xyz means the measure Sales Amount
kqb means the measure Profit Amount

…then closing and reopening the Copilot pane in Desktop and entering the prompt:

show kqb

…also returns the result you would expect, with kqb understood as Profit Amount

However, if you publish the same model up to the same workspace as before – so you are overwriting the previous version of the model in the Service – and then use the same prompt immediately after publishing:

…Copilot returns an incorrect result: it does not understand what “kqb” means. Why?

After you publish changes to a Power BI semantic model it can take a few minutes, sometimes up to an hour, for updates to the AI Instructions to be applied. This means if you’re testing Power BI Copilot in the Service you may need to be patient if you want to see the impact of any changes to AI Instructions, or do your testing in Power BI Desktop.

How can you know whether the latest version of your AI Instructions are being used in the Service when you do your testing? In the Power BI side pane in both Desktop and the Service there is an option to download diagnostics from the “…” menu in the top right-hand corner. This downloads a text file with diagnostic data in JSON format which contains a lot of useful information; most importantly it contains the AI Instructions used for the current Copilot session. The file contents aren’t documented anywhere, I guess because the structure could change at any time and it’s primarily intended for use by support, but there’s no reason why you as a developer shouldn’t look at it and use it.

For the second example in the Service above, where Copilot returned the wrong result, here’s what I found at the end of the diagnostics file:

As you can see the changes I made to the AI Instructions before publishing the second time had not been applied when I ran the prompt asking about kqb.

After waiting a while, and without making any other changes to the model, the same prompt eventually returned the correct results in the Service:

Looking at the diagnostics file for this Copilot session it shows that the new version of the AI Instructions was now being used:

Since looking in the diagnostics file is the only way (at least that I know of right now) to tell what AI Instructions are being used at any given time, it makes sense to do what I’ve done here and put a version number at the top of the instructions so you can tell easily whether your most recent changes are in effect.

One last point to mention is that if you’re deploying semantic models using Deployment Pipelines or Git, the docs state that you need to refresh your model after a deployment for changes to AI Instructions to take effect and that for DirectQuery or Direct Lake (but not Import) mode models this only works once per day.

Power BI Copilot, AI Instructions And Visualisation Guidelines

If there are specific ways you want your data to be visualised by Power BI Copilot then you have two options. You can use Verified Answers to link questions to visuals on your reports and can even set them up so users can apply filters to these visuals in their prompts. Alternatively – and this is the topic of this post – you can use AI Instructions to tell Copilot which visuals should be used when certain combinations of measures and columns appear together in a prompt.

Using the same semantic model I have used in this series of posts, consider the following prompts and the responses returned when there are no AI Instructions:

show count of transactions by date for the first week of January 2025

This returns results as a line chart:

show count of transactions broken down by New

[There is a column in the model called New containing Y or N values]

This returns results as a bar chart:

Adding the following AI Instructions changes the visualisations used by Copilot for these prompts:

When showing the count of transactions measure by date always use a column chart.
When showing the count of transactions measure by New use a pie chart.

Here are the responses returned now:

show count of transactions by date for the first week of January 2025
show count of transactions broken down by New

Before someone comments, I’m not saying that these visualisation choices are better than the defaults, I’m just saying that you can use AI Instructions to change the visuals used in responses. There may be a type of analysis for your data or your industry that should always use a certain Power BI visual, one that isn’t the visual that Copilot chooses by default. And we all know that some users have strong opinions on which visuals should be used that don’t follow data visualisation best practices…

Power BI Copilot, AI Instructions And Semantic Model Relationships

Power BI Copilot knows about the relationship between measures and tables, which means that it’s very good at knowing which measures can be broken down by which columns. For example, consider the following model with two fact tables, one of which contains sales data dimensioned by Employee and Customer, and one which contains target data that is only dimensioned by Employee:

For extra fun, I’ve created a disconnected measure table to hold all the measures, which are defined as follows:

Sales Amount = SUM(Sales[Sales])
Target Amount = SUM(Targets[Target])
Target % = DIVIDE([Sales Amount],[Target Amount])

Without any AI Instructions added to the model, for the following prompt:

Show Sales Amount broken down by Customer

I get (as you would expect) the following correct response:

However for the prompt:

Show Target Amount broken down by Customer

…I get a response that you can’t show Target Amount broken down by Customer because there’s no direct relationship. This is, I think the right response for most users.

Note that Copilot knows the relationships between the Sales Amount and Target Amount measures and the Customer dimension table even though they are on the disconnected measures table, which I think is very smart.

Even better, a prompt like this:

show a table with Employee Name, Customer Name, Sales Amount, Target Amount and Target %

Returns the result I would expect:

Copilot has understood that even though there’s no relationship between Customer and Target Amount, it makes sense to break it down in this case.

What about calculation groups? I’ve seen some inconsistent behaviour over the last few days but on the day I’m writing this post, calculation groups were working well in Copilot with no special AI Instructions despite the fact they have no relationship with any other table.

Adding a calculation group to the model like so with two calculation items, Actual Value and Forecast:

Actual Value = SELECTEDMEASURE()
Forecast = SELECTEDMEASURE() * 100

And prompting:

Show Forecast Sales Amount broken down by Customer

…gives the right response using the Forecast calculation item:

That said I’m sure there will be scenarios where Copilot decides it can’t use a measure with a particular column even though you want it to, and you can override this behaviour with AI Instructions. However, I have found you need to be very careful about what instructions you provide to get the output you expect. For example adding the following to the AI Instructions of my model:

For the Target Amount measure, ignore any previous instructions regarding not displaying measures with columns from tables where there is no direct relationship. 

…means that the prompt shown above which asks for Target Amount broken down by Customer:

Show Target Amount broken down by Customer

…returns the following result:

This surprised me a lot: I was expecting to see the same value repeated across all customers. I worked out that the reason different customers showed different values for Target Amount was that the visual was assuming an indirect relationship via the Sales table, but even then I had no idea that it was possible to build a Power BI visual that did this when no many-to-many relationships exist in the model – I certainly couldn’t build the same visual myself manually. It’s one way of answering the question correctly though.

Altering the AI Instructions to tell Copilot not to do this was tricky but eventually I came up with this:

For the Target Amount measure, ignore any previous instructions regarding not displaying measures with columns from tables where there is no direct relationship. When displaying Target Amount by Customer do not assume there is an indirect relationship via the Sales table.

The same prompt then gave me the result I was expecting with the same Target Amount value repeating across all customers (and it’s interesting it decided it had to create a DAX query to do this too):

This tells me that it’s probably not a good idea to try to override Copilot’s default behaviour when it comes to relationships, tables and measures.

Power BI Copilot, AI Instructions And Dealing With Ambiguity

One of the most common questions I hear about Power BI Copilot is how you can stop it from guessing what a user means when they ask an ambiguous question, and instead get it to ask for clarification. This is an interesting problem because Copilot already does this and what you really want is a way to control the level of tolerance for ambiguity. What’s more, if Copilot guesses what the user means correctly you’re probably not going to complain or even notice; it’s only when it guesses incorrectly that you’re going to wish it had asked what the user meant.

Using the semantic model containing UK real estate sales data that I’ve used throughout this series of posts, with no AI Instructions added to the model, consider the following prompt:

where is the nicest place to live in England?

It’s a great example of a question that could potentially be answered from the data but only with more information on what the user means by “nicest”. I think Copilot comes back with a very good response here:

As you can see, Copilot doesn’t know what the user means by “nicest” and asks the user what criteria they want to use to determine whether a place is “nice”.

What about an example of where Copilot does make assumptions about what the user means? Take the following prompt:

show how the number of sales varied recently

This time it does come back with an answer. I think this is a good, useful response but I want you to notice that Copilot has made two assumptions here:

  1. It has interpreted “number of sales” as meaning the measure Count of Transactions
  2. It has taken the term “recently” and decided to show date-level data for the latest available month of data, April

Can this behaviour be changed? Yes, but it’s not an exact science. For example adding the following to the AI Instructions of the model:

If you don't understand what the user is asking for never, ever guess - you must always ask for clarification

Here’s what Copilot now responds to the second prompt above:

As you can see, it’s now asking what is meant by “recently”. Clarifying this as follows:

"recently" means the days in April

Gives the same result as without the AI Instructions:

BUT – even though Copilot asked what “recently” meant, it still went ahead and assumed that “number of sales” meant the Count of Transactions measure. Adding to the AI Instructions to make it clear that Copilot should always ask if there’s any doubt about which measure to use like so:

If you don't understand what the user is asking for never, ever guess - you must always ask for clarification. In particular if the user does not explicitly mention the exact name of the measure they want to use, or the name of the measure is in any way ambiguous, do not return a result. Instead ask the user which measure they want to use. Be extremely cautious about which measure you use.

…results in a response that asks for clarification not only about what “recently” means but also what “number of sales” means:

Copilot doesn’t do this reliably though: even though it always seems to ask about “recently” now it only sometimes asks for clarification about “number of sales”. Sometimes is better than never though; indeed it’s the kind of uncertainty that is expected with Generative AI. I think I need to do some more research into what’s going on here. At least this shows that AI Instructions can be used to make Copilot more cautious around ambiguous questions and more likely to ask for clarification.

[Update 17th July 2025]

After testing this some more (and possibly after an update to the Power BI Service in the last few days) I have come up with some AI Instructions that seem to be a lot more reliable when it comes to asking the user which measure they want to use and to define what “recently” means:

If you don't understand what the user is asking for never, ever guess - you must always ask for clarification. If there are multiple points you don't understand it is essential that you ask the user to clarify all of them.

In particular, ignore all previous instructions regarding which measure to select and make sure you obey the following rule: if the user does not explicitly mention the exact name of the  measure they want to use, or the name of the measure is in any way ambiguous, do not return a result. Instead ask the user which measure they want to use. Be extremely cautious about which measure you use! At the same time, as I said, remember to clarify other, non-measure related ambiguities.

Power BI Copilot, AI Instructions And Preventing The Use Of Implicit Measures

In yet another entry in my series on what you should be doing in Power BI Copilot AI Instructions, in this post I want to address the most difficult (in terms of deciding what to do, rather than how to do it) topic: whether you should allow the creation of implicit measures.

A quick recap of terminology: implicit measures are measures that are automatically created when you drag and drop a field into a visual and tell Power BI how the data should be aggregated (for example by summing, counting etc); explicit measures are measures that are specifically defined by a semantic model developer, have a name and have a DAX expression that specifies what data should be aggregated and how it should be aggregated. There is a property in a semantic model called “discourage implicit measures” that prevents report designers from creating implicit measures in the UI but it is by no means foolproof and at the time of writing Power BI Copilot does not respect it. This property was created for use with calculation groups but I’m going to leave the subject of Copilot and calculation groups for a future post.

Let’s see an example of an implicit measure created by Copilot. In the model I’ve been using in this series I have only two explicit measures, defined as follows:

Average Price Paid = AVERAGE('Transactions'[Price])
Count Of Transactions = COUNTROWS('Transactions')

The following prompt:

Show the number of distinct postcodes by month

Returns the following correct result by creating an implicit measure that does a distinct count on the Postcode column:

This is great – so why would you ever want to prevent this from happening and stop the use of implicit measures?

First of all, there’s always a danger with implicit measures that an end user – whether they are using Copilot or not – will try to aggregate data in a way that it should not be aggregated and therefore end up with incorrect or misleading results. For example the prompt:

Show the sum of month number by county

Returns a result, but not one that makes sense because you should never sum up values in the month number column:

[And this is despite the Summarization property of the Month Number column being set to “Don’t summarize”, which I need to talk to someone about]

Second, in complex semantic models, it may be necessary to include business logic in every explicit measure to ensure they always return the results you want. Implicit measures will not contain this logic and will therefore not return correct results.

As a result, if you’re sure you have created all the explicit measures an end user could ever need (and that’s definitely an achievable goal for many semantic models) then preventing the use of implicit measures could be a good thing.

Adding the following text to the model’s AI Instructions to prevent the use of implicit measures:

Never aggregate data using implicit measures. Only ever use the explicit measures defined in the semantic model to show aggregated data. If the user asks to aggregate data from a column, tell them that you aren't allowed to do this because of the risk of showing incorrect data.

Means that the following prompt from above:

Show the number of distinct postcodes by month

Now returns a message saying that Copilot isn’t allowed to aggregate data by itself:

As you can see, it is possible to use AI Instructions to prevent the use of implicit measures but this is something you really need to think hard about: it may help stop users getting incorrect results but it could also stop users from getting the correct results they need in some cases. It’s the old struggle between centralised, “the developer knows best” BI and decentralised self-service BI all over again.

Data Validation In Power BI Copilot AI Instructions

Here’s yet another post in my series on things I think you should be doing in Power BI Copilot AI Instructions. Today: validating values that users enter as filters in their prompts. It’s something of a companion piece to last week’s post about helping users understand what data is and isn’t in the semantic model, because the more I think about it, the biggest problem users have when trying to query data in natural language is knowing about the data that is there to be queried. As I said last week, if you’re an end user interacting with a Power BI report via a slicer or a filter you know what values are available to choose because you can see them listed in the slicer or the filter – but you don’t see them when composing a prompt. As a developer or someone doing a demo it’s easy to forget this because you know the data so well but for an end user it’s not so easy and so they need all the help that the model developer can give them.

Let’s see an example using the semantic model that I’ve been using in this series containing UK real estate sales data. The Transactions table in my semantic model contains one row for each property sold; each property’s address is given and each address has a UK postcode (something like a US zip code – I’m sure all countries have an equivalent).

Everyone in the UK knows their postcode and a postcode contains a wealth of geographic information, as this section of the Wikipedia article on postcodes shows. There’s no need to get too detailed on their format though because I want to point out one important feature of all of the properly-formatted postcodes in the Transactions table shown above: they all have a space in the middle of them. And people being people, when they use postcodes, they usually forget that and write a postcode without the space.

This has consequences for Power BI Copilot. For example, the prompt:

show count of transactions for the postcode YO89XG

Returns a message saying that Copilot can’t find any data for the postcode “YO89XG”. This is because the postcode doesn’t contain a space. This is what you might expect as a developer but it will not make much sense to an end user.

On the other hand if the postcode in the prompt does contain a space in the right place, like so:

show count of transactions for the postcode YO8 9XG

…it returns the desired result:

How can we address this specific issue? Fairly easily, it turns out, because UK postcode formats are well documented and I would imagine Copilot has been trained on the same Wikipedia page on postcodes that I linked to above. As a result, adding the following to the AI Instructions for my semantic model:

The postcode column contains postcodes for locations in England and Wales. If the user enters a value to filter by for postcode that returns no data and the value is in an invalid format for a postcode, tell them they appear to have made a mistake, explain why the postcode format is wrong and suggest some changes to the value entered by the user that might result in a valid postcode.

Means that when I use the first prompt above, for the postcode without the space, I get a much more helpful response:

Clicking on the first option in this screenshot alters the prompt to include a space in the right place, which results in the user seeing the desired data:

I was encouraged by this, but there’s one obvious problem here: this only works for data like UK postcodes where the format is widely known. The format of your company’s invoice numbers is unlikely to be something that Copilot knows about.

So I experimented with using regular expressions in my AI Instructions and guess what, they seemed to work really well! But then I stopped to think – could I really trust an LLM to use a regex to validate values? The good thing about working at Microsoft is that I have a bunch of friendly colleagues who know way more about AI than I do so I asked them this question. One of them told me that for Copilot to properly validate data using regexes it would need to write some code and it can’t do that yet; instead it’s probably interpreting what the regex is looking for and trying to match the value against the interpretation. So while it might appear to work it would be prone to making errors.

Damn. That meant that if the LLM made a mistake when validating the data before running the query it would run the risk of preventing the user from filtering by a valid postcode, which would not be good. But then I thought, what if I applied the validation after it was clear that the user had entered a postcode that returned no data? That way it would be less important if the LLM made a mistake in its check because it would only happen when it was clear the user needed extra help.

Writing the AI Instruction to only validate the data after checking to see if the value the end user was filtering on didn’t exist seemed to work. Here’s the AI Instruction using a regex I found here to validate UK postcodes:

The postcode column contains postcodes for locations in England and Wales. UK postcodes must follow the format described in the following regular expression:
^([A-Za-z]{2}[\d]{1,2}[A-Za-z]?)[\s]+([\d][A-Za-z]{2})$
If the user enters a value to filter by for postcode that returns no data and the value is in an invalid format for a postcode, tell them they appear to have made a mistake, explain why the postcode format is wrong and suggest some changes to the value entered by the user that might result in a valid postcode.

Note how I say “If the user enters a value to filter by for postcode that returns no data…”

Here’s the result for the followng prompt asking for data for an invalid postcode:

show count of transactions for the postcode E48QJJ

I did some other tests on sample data and it does indeed suggest that the wording you use in the AI Instruction can control whether Copilot tries to validate the data before checking if the value the user is filtering on exists (which, as I said, would be bad because of the risk of it making a mistake when trying to validate data) or after (which, as I said, is a lot less dangerous).

All in all it seems that putting some thought into data validation in AI Instructions can result in a much friendlier end user experience in Copilot. That said, I doubt that however good your AI Instructions the experience will ever match the experience of seeing a list of possible values in a filter or slicer. Maybe what we need is something like IntelliSense when writing a prompt so you can see and search for values in your data?

Power BI Copilot AI Instructions: Helping Users Understand The Scope Of The Data

Continuing my series of posts on Power BI Copilot and the type of things you should be including in AI Instructions, today I want to talk about helping the end user understand the scope of the data: what data your model does contain, what data it could contain but doesn’t, and what data it could never contain.

Let me show you some examples to explain what I mean, using the semantic model I introduced in the first post in this series containing real estate transaction data from the Land Registry in the UK. On a model with no AI Instructions added, if I give Power BI Copilot the prompt:

show average price paid for the city manchester

I get the correct answer back:

That’s good and as you would expect. Now consider the following two prompts which both return no data:

show average price paid for the city edinburgh
show average price paid for the locality Boode

A user asking a question and not getting an answer is unavoidable in many cases but it still represents a failure: the user wanted information and the model couldn’t help. As a semantic model developer you need to minimise the impact of this and help the user understand why they didn’t get an answer. In both cases Copilot does the right thing and tells the user that it can’t find data for these places. However, there’s an important difference between these two places.

Boode is a very small village (somewhere I found at random) in England. There’s no data for it in the semantic model because there happen to be no real estate sales for it. Edinburgh on the other hand is a large city, so why is there no data? It’s because this semantic model only contains data for England and Wales, not Scotland, Northern Ireland or anywhere else. An end user might expect that a dataset from a UK government agency would contain data for the whole of the UK but this one doesn’t. If the user was looking at a report displaying this data then this information would probably be displayed somewhere in a textbox on an information page, or be obvious from the values visible in slicers or filters, but with Copilot we have to tell the user this in other ways.

Similarly, consider the following two prompts:

show average price paid for January 2025
show average price paid for January 2024

Why is a blank displayed for January 2024? Were there no sales in January 2024? The answer is that the semantic model only contains data between January 1st 2025 and April 30th 2025, so there’s no point asking for data before or after that – and we need to tell the user so.

Here is some text that I added to the AI Instructions for this semantic model to tell Copilot what data is and isn’t present:

##What data this semantic model contains
This semantic model only contains data for England and Wales. If the user asks for data outside England and Wales, tell them that this model does not contain the data they are looking for.
This model only contains data for the date range 1st January 2025 to 30th April 2025. Tell the user this if they ask for data outside this date range.

With these instructions in place let’s rerun some of the prompts above. First, let’s ask about Edinburgh again:

This is a much more helpful response for the user I think. Copilot knows that Edinburgh is in Scotland and it tells the user that it only has data for England and Wales. Copilot doesn’t know every place name in Scotland but it does pretty well; for example (to take another random place name) it knows that Arrina is in Scotland:

Similarly, here’s what Copilot now says for January 2024:

Again, a much better response than before: it explicitly tells the user that the model only contains data for January 1st 2025 to April 30th 2025.

Giving Power BI Copilot clear instructions on what data the semantic model does and doesn’t contain means that it can set end users’ expectations correctly. This then means that users are more likely to ask questions that give them useful information and therefore builds trust. Conversely, not explaining to end users why their questions are returning data means they are less likely to want to use Copilot in the future.