Category: Aether

Aether

  • Running DAX Queries with PowerShell

    Running DAX Queries with PowerShell

    What if you could write a DAX query once and run it against every semantic model you have access to, in a single script?

    That’s what this post covers. You take a query you already trust, point PowerShell at your workspaces, and get the results back from every model, along with a list of the ones that failed. No clicking through models one at a time.

    I’m actively looking at automating DAX queries across multiple workspaces, whether by script, Power Automate or an AI agent. I’ve started with PowerShell, and the reason is the data I work with.

    I work with financial data for large companies around the world. That is not where you want a confident, plausible looking number that turns out to be hallucinated.

    PowerShell gives me a baseline. I can build up a library of DAX queries that I know return exactly what I need, because I’ve already built and validated them in Power BI. When I move on to agentic AI, that library doesn’t go to waste. It becomes the set of trusted tools an agent can call, rather than something the agent has to invent.

    Script first, agent later.

    It also has plenty of uses in its own right, and it’s relatively simple to set up.

    So, here is one use case I’m using it for right now: reviewing Copilot Cowork usage across all my customers.

    I’ve already built a visual that combines exactly what I need, so I copy the query behind it and run it against every semantic model. Opening each customer’s report to check by hand would take far too long, and this way the numbers come straight from a visual I already trust.

    Quick and easy, and reducing that hallucination risk as I build out further with data agents.

    If you’ve read my post on running DAX queries from Power Automate

    How to Connect Power BI to Power Automate: Running DAX Queries | JamesMM Aether

    this will look familiar, because the query comes from the same place.

    • Open your report in Power BI Desktop
    • Go to Optimize and select Performance Analyzer
    • Click Start recording, then Refresh visuals
    • Find the visual you want and choose Copy query or Run in DAX query view

    (I’ve used the same examples from the other post to keep things consistent)

    Run it in DAX query view and you’ll see the raw DAX and the output it produces.

    Here’s the full script. The sections below explain what each part does.

    # ============================
    # POWERBI Query Runner
    # James Mounsey-Moran
    # ============================
    
    # Dataset names to include or exclude
    # Blank returns all, separate with | for multiple
    $include = ""
    $exclude = ""
    
    # ============================
    # DAX QUERY
    # ============================
    
    $daxQueryTemplate = @"
    ###ADD YOUR QUERY HERE
    "@
    
    # ============================
    # VARIABLES
    # ============================
    
    $successfulDatasets = @()
    $failedDatasets = @()
    
    # ============================
    # CONNECT TO POWER BI
    # ============================
    
    try {
        Connect-PowerBIServiceAccount -ErrorAction Stop
    }
    catch {
        Write-Error "Failed to connect to Power BI Service: $($_.Exception.Message)"
        return
    }
    
    # ============================
    # GET WORKSPACES
    # ============================
    
    try {
    
        # To scan all workspaces:
        $workspaces = Get-PowerBIWorkspace -All -ErrorAction Stop
    }
    catch {
        Write-Error "Failed to retrieve workspaces: $($_.Exception.Message)"
        return
    }
    
    # ============================
    # PROCESS DATASETS
    # ============================
    
    foreach ($workspace in $workspaces) {
    
        Write-Output ""
        Write-Output "Processing workspace: $($workspace.Name) ($($workspace.Id))"
    
        try {
            $datasets = Get-PowerBIDataset -WorkspaceId $workspace.Id -ErrorAction Stop |
                Where-Object {
                    (-not $include -or $_.Name -match "(?i)$include") -and
                    (-not $exclude -or $_.Name -notmatch "(?i)$exclude")
                }
        }
        catch {
            Write-Warning "Failed retrieving datasets from workspace $($workspace.Name)"
            continue
        }
    
        if (-not $datasets) {
            Write-Output "No datasets found. Skipping."
            continue
        }
    
        foreach ($dataset in $datasets) {
    
            Write-Output "  Processing dataset: $($dataset.Name)"
    
            try {
    
                $response = Invoke-PowerBIRestMethod `
                    -Url "datasets/$($dataset.Id)/executeQueries" `
                    -Method Post `
                    -Body (@{
                        queries = @(
                            @{
                                query = $daxQueryTemplate
                            }
                        )
                        serializerSettings = @{
                            includeNulls = $true
                        }
                    } | ConvertTo-Json -Depth 10) `
                    -ErrorAction Stop
    
                $json = $response | ConvertFrom-Json
    
                if (-not $json.results) {
                    throw "No results returned."
                }
    
    
                $rowCount = @($json.results[0].tables[0].rows).Count
                $results = $json.results[0].tables[0].rows
    
                Write-Output "    SUCCESS ($rowCount rows)"
    
                $successfulDatasets += [PSCustomObject]@{
                    Workspace = $workspace.Name
                    Dataset   = $dataset.Name
                    Data      = $results
                    DatasetId = $dataset.Id
                    Rows      = $rowCount
                    Timestamp = Get-Date
                }
    
            }
            catch {
    
                Write-Output "    FAILED"
    
                $failedDatasets += [PSCustomObject]@{
                    Workspace = $workspace.Name
                    Dataset   = $dataset.Name
                    DatasetId = $dataset.Id
                    Error     = $_.Exception.Message
                    Timestamp = Get-Date
                }
    
            }
    
        }
    
    }
    
    # ============================
    # Results
    # ============================
    
    Write-Output ""
    Write-Output "============================="
    Write-Output "Results"
    Write-Output "============================="
    
    Write-Output "Successful datasets: $($successfulDatasets.Count)"
    Write-Output "Failed datasets: $($failedDatasets.Count)"
    
    if ($successfulDatasets.Count -gt 0) {
    
    
        $successfulDatasets |
            Select-Object Workspace, Dataset |
            Format-Table -AutoSize
    }

    First of all ensure you have the PowerBI modules installed for PowerShell (you may do already)

    Install-Module MicrosoftPowerBIMgmt

    The script signs in with:

    Connect-PowerBIServiceAccount

    If that fails, it stops and tells you why.

    It then pulls your workspaces with:

    Get-PowerBIWorkspace -All

    Which returns every workspace you’re a member of.

    Paste the query you copied earlier in place of

    “###ADD YOUR QUERY HERE”

    $daxQueryTemplate = @"
    ###ADD YOUR QUERY HERE
    "@

    The closing "@ has to sit at the very start of its own line, or PowerShell will most likely complain.

    Set $include and $exclude to limit which models get queried. Leave them blank to hit everything, and separate several names with |. These are regular expressions, so a model name containing characters like ( or . can behave unexpectedly.

    This matters more than it looks. If your query references a measure that doesn’t exist in every model, the others will fail, and the filter keeps that noise out.

    For each workspace, the script lists the models, applies your filters and sends the query to the executeQueries endpoint. Each model that returns data goes into:

     $successfulDatasets

    Anything that errors goes into $failedDatasets along with the error message, so you can see what went wrong.

    When the script finishes, you get a summary of how many models succeeded and failed.

    The data is held in $successfulDatasets, so you can inspect any model directly. For example, to look at the second one (Yes I know it says 1, the first would be 0):

    $successfulDatasets[1]
    $successfulDatasets[1].Data

    To keep the results, pipe them to a file with Export-CSV if you need to!

    This is the first step towards something bigger. Once you have a library of queries you trust, you can schedule the script, feed the results into Power Automate, or give them to an AI agent as tools it can call, with answers you can actually rely on.

    That’s where I’m heading next.

  • How to Get Exact Number Input in Power BI

    How to Get Exact Number Input in Power BI

    One component I used when building my recent Copilot Credits Calculator was the Power BI input slicer.

    See it here > Copilot Credits: Managing Consumption – Home

    And out of the whole report it’s one area that the Power BI community messaged me about the most: “How do you get exact number input in Power BI?”

    I had a requirement for users of the report to be able to accurately input the number of end users overall within their tenant. This would then be used as part of a numeric series to combine with measures and calculate the desired results.

    Now if you have ever used numeric ranges and used the slider that you can auto add when creating, if creating a relatively large series with potentially an increment of 1, it won’t be as accurate as you might be looking for.

    For example, generate a series of 100,000 and then try typing in a number. I typed in 45,678 and this is what the slicer returned (45,654). Not exact, not accurate. Not so great when we are trying to define financial potential.

    The input slicer, thankfully works a little differently! Instead of using the usual numeric range, let’s make our own instead.

    • Create a new table
    TotalUsersSeries = GENERATESERIES(1,100000,1)

    So a series of 1 to 100,000 with an increment of 1.

    GENERATESERIES function – DAX | Microsoft Learn

    In this instance the table can be completely disconnected from the rest of the model, so negligible impact to performance here.

    • Then create a measure, as below so we can use it within other measures and calculations. Worth noting the 1000 at the end of the measure is to give the measure a default value.
    TotalUsers = SELECTEDVALUE(TotalUsersSeries[Value],1000)

    SELECTEDVALUE function – DAX | Microsoft Learn

    • Next add in your Input Slicer and add the [Value] column from the TotalUsersSeries table.

    And there you have it! An accurate input box for what-if scenarios. Format the slicer and change the default text so it suits your report.

  • Copilot Credits: Managing Consumption

    Copilot Credits: Managing Consumption

    “Nobody actually knows how many ‘high usage’ requests a user makes, but everyone knows their headcount and adoption trajectory”

    The next stage of Copilot adoption, and the next step in Microsoft Investment Management is managing Copilot Credits.

    Prism, a solution I originally built to manage Microsoft Investment captures a significant amount of data across M365, Azure, Copilot & Security. Copilot specifically showcases who is using aspects of Copilot, Cowork, Agents. All showing how many times, how often and by who!

    So using data from the Prism platform, we can look at the median number of users using different features across all tenants. From that, I built a model that predicts actual credit usage, based on the actual deployments of actual customers.

    Many Copilot Credit calculators rely on sliders and guessed figures that, let’s be honest, nobody really knows how to set. How many “high usage” requests is a user going to make in a month? It’s all guesswork nobody actually knows the answer.

    What we do know is how many users we have in a tenant, and everyone can give a reasonable idea of how their Copilot adoption is progressing.

    My Copilot Credit Planner uses all of this, and more. Combined with the latest Copilot Credit Usage guidelines (August 2026), it predicts the expected number of credit-consuming users, as well as the estimated number of credits required.

    The model also shows the recommended P3 tier (Copilot Credit Annual Prepayment) versus the Annual PAYG cost, and how pricing shifts as you combine tiers or step up to the next one.

    Have a try! Any feedback get in touch, and if you want to see your actual Credit Copilot usage feel free to drop me an email at james@jamesmm.com or my work email at james.mounsey-moran@trustmarque.com