How To Export M365 Licenses With Microsoft Graph PowerShell

The Get-MgSubscribedSku cmdlet in Microsoft Graph PowerShell can help you retrieve information on the license subscription available in your tenant. Some information however is hidden within nested attributes and not immediately available by just running the Get-MgSubscribedSku alone.

In this tutorial, I am going to show you how you can use a Microsoft Graph PowerShell script to export a report of the total available and total consumed licenses in your Microsoft 365 Tenant.

Pre-requisites

To be unable to run the script you will need to have Microsoft Graph PowerShell installed and running the latest version. Check out my tutorial on installing Microsoft Graph PowerShell, you can use the same guide to update your existing version of Microsoft Graph PowerShell.

Export M365 license consumption to a CSV file

Below you can see the full script to build and export a report of the consumed licenses in your Microsoft 365 tenant. 

For this script to run, you must define the file path and the file name for the output report. You should also ensure that your user account has access to the area you want the file saved.

$path = "C:\temp" #Define the output file path
$Filename = "licenses2.csv" #Define the file name

Import-Module Microsoft.Graph.Identity.DirectoryManagement
Connect-MgGraph -Scope Directory.Read.All

$Report = [System.Collections.Generic.List[Object]]::new()
$licenses = get-MgSubscribedSku
Foreach ($license in $licenses){
    $obj = [PSCustomObject][ordered]@{
        "License SKU" = $license.SkuPartNumber
        "Total Licenses" = $license.PrepaidUnits.Enabled
        "Used Licenses" = $license.ConsumedUnits
    }
    $report.Add($obj)
}

Write-host "Exporting license information report to" $path -ForegroundColor Green
$Report

try {$report | Export-CSV -path ($path + "\" + $filename) -NoTypeInformation}
catch { "Export path is not valid or accessible" }

As you can see, the script uses the Get-MgSubscribedSku cmdlet from the Microsoft Graph Identity Management module. You can find more detail on the cmdlet by using the the Find-MgGraphCommand cmdlet like the below.

Find-MgGraphCommand -Command Get-MgSubscribedSku | FL

Your output will give you similar, if not the same information as below.

Command : Get-MgSubscribedSku
Module : Identity.DirectoryManagement
APIVersion : v1.0
Method : GET
URI : /subscribedSkus/{subscribedSku-id}
OutputType : IMicrosoftGraphSubscribedSku
Variants : {Get, GetViaIdentity}
Permissions : {Directory.Read.All, Directory.ReadWrite.All, Organization.Read.All}

Command : Get-MgSubscribedSku
Module : Identity.DirectoryManagement
APIVersion : v1.0
Method : GET
URI : /subscribedSkus
OutputType : IMicrosoftGraphSubscribedSku
Variants : {List}
Permissions : {Directory.Read.All, Directory.ReadWrite.All, Organization.Read.All, Organization.ReadWrite.All}

If you are interested in learning more about Microsoft Graph PowerShell, be sure to sign up for our mailing list here, where at the end of each week we will release a single email update containing any new and feature tutorials!

Daniel Bradley

My name is Daniel Bradley and I work with Microsoft 365 and Azure as an Engineer and Consultant. I enjoy writing technical content for you and engaging with the community. All opinions are my own.

This Post Has 8 Comments

  1. Matt

    Hi Daniel,

    This is really useful script to manage purchased/used licenses. Is there any way to exclude free licenses such as Flow free, Stream etc.

    Thanks

    1. Daniel Bradley

      Hi Matt,

      Of-course, luckily I think all of the Free licenses have the string “Free” in the SKU Part number. Unfortunately Get-MgSubscribedSku does not support the -Filter parameter, however you can simply replace:

      $licenses = Get-MgSubscribedSku WITH $licenses = Get-MgSubscribedSku | Where {$_.SkuPartNumber -notmatch “Free”}

  2. Matt

    Thanks Daniel.

    It is removing FLOW_FREE only so I tried Where-Object {@(‘POWER_BI_STANDARD’,’FLOW_FREE’,’MICROSOFT_BUSINESS_CENTER’,’STREAM’,’POWERAPPS_DEV’) -notcontains $_ } but that didn’t work. I will do bit more research and if I can get it working I will post it on here.

    1. Daniel Bradley

      The curly braces {} define a script block, so you can use a script like the following to get your desired result:

      Get-MgSubscribedSku | Where {
      $_.SkuPartNumber -notmatch 'FLOW_FREE' -and
      $_.SkuPartNumber -notmatch 'POWER_BI_STANDARD' -and
      $_.SkuPartNumber -notmatch 'MICROSOFT_BUSINESS_CENTER' -and
      $_.SkuPartNumber -notmatch 'STREAM' -and
      $_.SkuPartNumber -notmatch 'POWERAPPS_DEV'
      }

      Or even simpler:

      $Match = @(‘POWER_BI_STANDARD’,’FLOW_FREE’,’MICROSOFT_BUSINESS_CENTER’,’STREAM’,’POWERAPPS_DEV’)
      Get-MgSubscribedSku | Where {$_.SkuPartNumber -notin $match}

  3. Matt

    Thanks Daniel,
    Below is the full script I am using now and it’s working perfectly

    $Report = [System.Collections.Generic.List[Object]]::new()
    $Match = @(‘POWER_BI_STANDARD’, ‘FLOW_FREE’, ‘MICROSOFT_BUSINESS_CENTER’, ‘STREAM’, ‘POWERAPPS_DEV’, ‘SPZA_IW’)
    $licenses = Get-MgSubscribedSku | Where-Object {$_.SkuPartNumber -notin $Match}

    foreach ($license in $licenses) {
    $sku = (Get-MgSubscribedSku -SubscribedSkuId $license.Id).SkuPartNumber
    $licenseCount = (Get-MgSubscribedSku -SubscribedSkuId $license.Id).PrepaidUnits.Enabled
    $usedLicenses = (Get-MgSubscribedSku -SubscribedSkuId $license.Id).ConsumedUnits

    $obj = [PSCustomObject]@{
    “License SKU” = $sku
    “Total Licenses” = $licenseCount
    “Used Licenses” = $usedLicenses
    }
    $Report.Add($obj)
    }

    $path = “******.csv”
    $Report | Export-Csv -Path $path -NoTypeInformation

    Write-Host “Exporting license information report to $path” -ForegroundColor Green
    Disconnect-MgGraph

    1. Daniel

      That’s great, well done for getting it working!

  4. JohnDoe

    Useful information here providing actual examples on scope required for connecting that’s not documented in single example with the command on MSFT site.

    One thing I don’t understand is the repeated calls like below. SkuPartNumber is retrieved by default and is part of the $license variable within the loop. There’s no obvious need to search for it again. If you were to ever do thousands or tens of thousands of licenses in the loop, the repeated calls for data you already have would really increase the amount of time required for the script to complete.

    (get-MgSubscribedSku -SubscribedSkuId $license.id).SkuPartNumber

    If there’s a property not returned by default, you should be able to use the -property on the very first call to provide a list all of the properties you want returned. Maybe there’s something I’m missing and a reason you did it this way. Very late for me.

    1. Daniel

      I generally have no idea why I did it the way I did. I often write very early in the morning 4-5am so I blame that! Anyway, I have updated the script 🙂 Thank you!

Leave a Reply