Script repository

Send notification if number of unused Microsoft 365 licenses falls below limit

Updated on: Jan 18, 2026, Views: 17506

Microsoft 365

The script sends an email notification if the number of unused licenses for any license plan enabled in any Microsoft 365 tenant registered in Adaxes falls below a specific limit. To execute the script, create a scheduled task configured for the Domain object type and add a managed domain to the Activity Scope of the task. The domain will only be used to trigger execution of the scheduled task.

Check all registered tenants

Parameters

  • $limit - the minimum number of unused licenses. The script will send an email if the number of available licenses is below this limit.
  • $skus - the SKU Part Numbers of the licenses to check. If set to $null, the script will check all the enabled licenses.
  • $to - the email address of the notification recipient.
  • $subject - the email notification subject.
  • $reportHeader - the report header.
  • $htmlTableCoulmns - the column headers for the table that contains details on the number of Microsoft 365 licenses.
  • $reportFooter - the report footer.
$limit = 5 # TODO: modify me
$skus = @("ENTERPRISEPREMIUM", "ENTERPRISEPACK") # TODO: modify me

# Email settings
$to = "recipient@domain.com" # TODO: modify me
$subject = "List of Microsoft 365 plans that have $limit or less licenses available" # TODO: modify me
$reportHeader = "<h2>List of Microsoft 365 plans that have $limit or less licenses available</h2>"
$htmlTableColumns = "
<table border='1'>
    <tr>
        <th>License Plan</th>
        <th>Total Licenses</th>
        <th>Unused Licenses</th>
    </tr>
" # TODO: modify me
$reportFooter = "<hr /><p><i>Please do not reply to this e-mail, it has been sent to you for notification purposes only.</i></p>" # TODO: modify me

# Find all Microsoft 365 tenants.
$microsoft365TenantsPath = $Context.GetWellKnownContainerPath("CloudServicesO365")
$searcher = $Context.BindToObject($microsoft365TenantsPath)
$searcher.Criteria = New-AdmCriteria "adm-O365Tenant"
$searcher.SearchScope = "ADS_SCOPE_SUBTREE"
$searcher.ReferralChasing = "ADS_CHASE_REFERRALS_NEVER"
$searcher.PageSize = 500

try
{
    $searchResultIterator = $searcher.ExecuteSearch()
    $searchResults = $searchResultIterator.FetchAll()
    
    $report = $NULL
    foreach ($searchResult in $searchResults)
    {
        # Bind to Microsoft 365 tenant.
        $tenant = $Context.BindToObject($searchResult.AdsPath)
        
        # Check licenses
        $htmlTable = $NULL
        foreach ($sku in $tenant.Skus)
        {
            if (($skus -ne $NULL) -and ($skus -notcontains $sku.SkuPartNumber))
            {
                continue
            }
            
            $difference = $sku.TotalUnits - $sku.ConsumedUnits
            if ($difference -gt $limit)
            {
                continue
            }
            
            if ($htmlTable -eq $NULL)
            {
                $htmlTable += "<h3>$($tenant.TenantName)</h3>$htmlTableColumns"
            }
            
            $htmlTable += "<tr><td>$($sku.DefaultDisplayName)</td><td>$($sku.TotalUnits)</td><td>$difference</td></tr>"
        }
        if ($htmlTable -eq $NULL)
        {
            continue
        }
        
        $htmlTable += "</table>"
        $report += $htmlTable
    }
    
    if ($report -eq $NULL)
    {
        return
    }
    
    # Build report
    $html = $reportHeader + $report + $reportFooter
    
    # Send mail
    $Context.SendMail($to, $subject, $NULL, $html)
}
finally
{
    # Release resources
    $searchResultIterator.Dispose()
}

Check only the specified tenant

Parameters

  • $limit - the minimum number of unused licenses. The script will send an email if the number of available licenses is below this limit.
  • $skus - the SKU Part Numbers of the licenses to check. If set to $null, the script will check all the enabled licenses.
  • $tenantName - the name of the Microsoft 365 tenant to check licenses in as it is specified in Adaxes.
  • $to - the email address of the notification recipient.
  • $subject - the email notification subject.
  • $reportHeader - the report header.
  • $htmlTableCoulmns - column headers for the table that contains details on the number of Microsoft 365 licenses.
  • $reportFooter - the email notification footer.
$limit = 5 # TODO: modify me
$skus = @("ENTERPRISEPREMIUM", "ENTERPRISEPACK") # TODO: modify me
$tenantName = "My tenant" # TODO: modify me

# Email settings
$to = "recipient@domain.com" # TODO: modify me
$subject = "List of Microsoft 365 plans that have $limit or less licenses available" # TODO: modify me
$reportHeader = "<h2>List of Microsoft 365 plans that have $limit or less licenses available</h2>"
$htmlTableColumns = "
<table border='1'>
    <tr>
        <th>License Plan</th>
        <th>Total Licenses</th>
        <th>Unused Licenses</th>
    </tr>
" # TODO: modify me
$reportFooter = "<hr /><p><i>Please do not reply to this e-mail, it has been sent to you for notification purposes only.</i></p>" # TODO: modify me

# Bind to the Microsoft 365 tenant.
$microsoft365TenantsPath = $Context.GetWellKnownContainerPath("CloudServicesO365")
$container = $Context.BindToObject($microsoft365TenantsPath)
$tenant = $container.GetObject("adm-O365Tenant", "CN=$tenantName")

# Check licenses
$htmlTable = $NULL
foreach ($sku in $tenant.Skus)
{
    if (($skus -ne $NULL) -and ($skus -notcontains $sku.SkuPartNumber))
    {
        continue
    }
    
    $difference = $sku.TotalUnits - $sku.ConsumedUnits
    if ($difference -gt $limit)
    {
        continue
    }
    
    if ($htmlTable -eq $NULL)
    {
        $htmlTable += "<h3>$($tenant.TenantName)</h3>$htmlTableColumns"
    }
    
    $htmlTable += "<tr><td>$($sku.DefaultDisplayName)</td><td>$($sku.TotalUnits)</td><td>$difference</td></tr>"
}

if ($htmlTable -eq $NULL)
{
    return
}

# Build report
$htmlTable += "</table>"
$html = $reportHeader + $htmlTable + $reportFooter

# Send mail
$Context.SendMail($to, $subject, $NULL, $html)

Comments 29

You must be signed in to comment.

  • Dave T.

    Dave T.

    Can the script be modified to only return values for the select products only or a specific subset of products?
    O365_BUSINESS_ESSENTIALS
    Office 365 Business

    • Support

      Support

      Hello Dave,

      Yes, it is possible. We have updated the script with the possibility to specify SKU Part Numbers of the licenses to check.

  • Amz

    Amz

    Running the script throws an error saying "You cannot call a method on null valued expression". Please help me fix. Have set $skus to NULL to check for all the license enabled in our tenant

    • Support

      Support

      Hello,

      Could you, please, send us (support[at]adaxes.com) the script you are using with all the modifications and a screenshot of the full error message you are getting?

  • GRes

    GRes

    Hello, I am receiving the same message as Amz. Did you update the script to fix this issue? Thanks.

    • Support

      Support

      Hello,

      We were not able to reproduce the issue and the script works just fine. Could you, please, send us (support[at]adaxes.com) the script you are using with all the modifications and a screenshot of the full error message you are getting?

      Also, please, specify how exactly the script is executed. A screenshot of the Scheduled Task that runs the script would be very helpful.

  • Jack

    Jack

    This worked great. Thank you!

  • Daniel

    Daniel

    Can we make it not send the email at all if all SKUs are good on the counts?
    We don't want a email at all unless it falls below on something.

    • Support

      Support

      Hello Daniel,

      Sorry for the confusion, but we are not sure what exactly you mean. The current script sends an email notification only if at least one of the specified Microsoft 365 licenses falls below the limit. If none of the licenses fall below the limit no email notification is send. If the email is still sent, please, make sure that you are using the exact above script without any modifications.

      • Daniel

        Daniel

        Yes sorry its working as expected. But is there a way to add multiple TO emails?

        • Support

          Support

          Hello Daniel,

          Yes, it is possible. Just specify all the addresses in the $to variable:

          $to = "recipient@domain.com, recipient2@domain.com"
          
  • Raul

    Raul

    How can I set different limits for different skus within the same email? or will it require a separate task for each limit?

    • Support

      Support

      Hello Raul,

      Unfortunately, there is no such possibility in the current script. However, it is possible to modify the script to meet your needs. Please, describe the desired behavior in all the possible details with live examples.

      • Raul

        Raul

        What we would like is a way to specify different limits that the script can select from based on limit quantity. So if there is one of the SKUs under $limit=10 is true, then it generates an email based on that, if there's one under the $limit=5 then it also generates one based on that, and so forth. I tried the following on my script, but it only executed it for $limit=3 and ignored the rest above:

        $limit = 10  
        $skus = @("EXCHANGESTANDARD","MCOMEETADV","SPE_E3","IDENTITY_THREAT_PROTECTION","MCOEV","POWER_BI_PRO")  
          
        $limit = 5  
        $skus = @("EXCHANGEENTERPRISE","POWER_BI_PRO","PROJECTPROFESSIONAL")  
          
        $limit = 3  
        $skus = @("VISIOCLIENT","MEETING_ROOM","PROJECT_P1")
        
        • Support

          Support

          Hello Raul,

          Thank you for clarifying. What about the email recipients? Should they be the same for all licenses?

          Also, please, specify how Microsoft 365 licenses should be identified in the email notifications. Should that be the licenses SKU Part Numbers or their display names as specified in the tenant settings?

          Any additional details will be much appreciated.

          • Shawn

            Shawn

            Two questions was there an update script for multiple limit's option that Raul asked about and if this is built as a scheduled task which object type should be selected.

            • Support

              Support

              Hello Shawn,

              >was there an update script for multiple limit's option that Raul asked about

              As you can see, our previous message remains unanswered and thus we were not able to write the script.

              >if this is built as a scheduled task which object type should be selected.

              As it is mentioned in the script description, the scheduled task should be configured for the Domain object type.

  • Raul

    Raul

    Hi Support,

    For our case only one recipient is needed; however, I can see how this could be really useful to be able to specify recipients to specific limits.

    For identification purposes, the display names of each SKU are good enough. If there's a way to also include the SKU part number along with the display name then I think this would be perfect.

    We also do manage a separate instance of Adaxes and MS tenant. I figured this will simply have to be managed separately with a script on that Adaxes admin console? Or is there a possibility to apply all of the above to multiple tenants from the same script? No big deal if this is not possible, but less management is always better :)

    • Support

      Support

      Hello Raul,

      > For our case only one recipient is needed; however, I can see how this could be really useful to be able to specify recipients to specific limits.
      We will update the script so that it is possible to specify a separate recipient for each license.

      > We also do manage a separate instance of Adaxes and MS tenant.
      If you have separate instances (not sharing common configuration) of Adaxes service you will need to configure the script execution separately on each of them.

      > Or is there a possibility to apply all of the above to multiple tenants from the same script?
      The script check licenses in all the Microsoft 365 tenants registered for the Adaxes service the script is executed by. If you need information about tenants included into the email, please, provide a sample email notification.

  • Casey

    Casey

    Hi,

    Is there a way to only report on the specific tenant rather than all the tenants?

  • Support

    Support

    Hello Casey,

    Yes, it is possible. We added the corresponding script (Check only the specified tenant) to the article.

  • VonZ

    VonZ

    getting "You cannot call a method on a null-valued expression."

    Looks like it's pointing to '"CloudServicesO365". What is that value exactly?

    Thanks!

    • Support

      Support

      Hello,

      The value is correct. For troubleshooting purposes, please, send us (support@adaxes.com) a full screenshot of the error you are facing.

  • Gareth

    Gareth

    Hi,

    I do not seem to have the domain-dns object type available when selecting the object type, where can I find it?

    • Support

      Support

      Hello Gareth,

      Could you, please, specify what version of Adaxes you are currently using? For information on how to check it, have a look at the following help article: https://www.adaxes.com/help/CheckServiceVersion.

      • Gareth

        Gareth

        Sure! My product version is 3.15.20817.0

        • Support

          Support

          Hello Gareth,

          Thank you for specifying. The Domain-DNS object type is not present in Adaxes 2023 that you are using. You need to select the Domain object type. The rest remains the same.

  • Ray Bilyk

    Ray Bilyk

    In line 5, how do you add an additional email address other than creating a group?
    $to = "recipient@domain.com" # TODO: modify me
    [Product version=3.16.21408.0]

    • Support

      Support

      Hello Ray,

      In your version of Adaxes, the following approach for multiple recipients should be used:

      $toAddresses = @("recipient@domain.com", "recipient2@domain.com") # TODO: modify me
      
      ...
      
      # Send mail
      foreach ($toAddress in $toAddresses)
      {
          $Context.SendMail($toAddress, $subject, $NULL, $html)
      }
      

Got questions?

Support Questions & Answers

We use cookies to improve your experience.
By your continued use of this site you accept such use.
For more details please see our privacy policy and cookies policy.