Skip to content

Use Power Automate or Logic Apps to keep an eye on your licenses

I guess we’ve all been there; you ran out of licenses in your Azure AD or Office 365 tenant. Despite you hang out in your admin portal every day, you were still taken by surprise when you discover an issue, caused by a license shortage. More often this is caused by the fact that the people who are responsible to buy these licenses, are not always IT admins. So it’s easy to run out of licenses. Time to get this fixed.

In this blog post, I am going to show you how you can keep an eye on your licenses, by alerting when a specific threshold is reached. In this example, I use a threshold of 95%, but depending on your use case, you can change your threshold.

What do we need for this?

To create these alerts, we use a flow in Power Automate, but you could also use Logic Apps for this. It’s up to you. Next, we’ll need an app registration in Azure AD with the right permissions to get a list of all the licenses, using Graph API. The HTTP function we use in Power Automate is a premium feature.

I am not going into much detail on how to create an app registration, or how the flow is built op. If you’re lost further on in this post, just take a look at my other post about Power Automate and Graph API.

App registration

Let’s start with creating an app registration in Azure AD. You can find your app registrations under Active Directory -> App registrations in the Azure portal. Give it a proper name, and leave the rest as is.

Next, make sure the app registration has the right permissions. Since we are using the List subscribedSkus API, you’ll need at least Organization.Read.All application permissions for Graph API. Don’t forget to configure the admin consent afterward.

Next, we’ll need a client secret for authentication. Create one and copy the secret to notepad. We need this in the next steps. Also, take note of the Application (client) ID en Directory (tenant) ID. So if you followed all the steps, you end up with:

  • An app registration with the right Graph API permissions
  • A client secret
  • The Application (client) ID
  • The Directory (tenant) ID

Create the flow

With all the previous steps in place, we can now go on and create a flow. Head over to you Power Automate portal, and start with an empty flow. Pick the scheduled flow to run this daily or weekly, depending on your needs.

Our first step is an HTTP request to pull out the license information from our tenant.

  1. Use the GET method
  2. Use the https://graph.microsoft.com/v1.0/subscribedSkus API
  3. Select Active Directory OAth as your authentication method
  4. Enter your tenant ID that you’ve captured in the previous chapter
  5. Use https://graph.microsoft.com as your audience url
  6. Enter your Client ID that you’ve captured in the previous chapter
  7. Enter your secret

This is a good moment to test your flow, to see if you have configured everything correctly. You should end up with the raw JSON data from the Graph API.

If everything works as expected, we can now go on and parse our data. Add a new step and select Parse JSON. Use the body of the HTTP request and use the output of your HTTP request as sample data to create the schema. Or you can use the sample data from the Microsoft documentation:

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscribedSkus",
    "value": [
        {
            "capabilityStatus": "Enabled",
            "consumedUnits": 14,
            "id": "48a80680-7326-48cd-9935-b556b81d3a4e_c7df2760-2c81-4ef7-b578-5b5392b571df",
            "prepaidUnits": {
                "enabled": 25,
                "suspended": 0,
                "warning": 0
            },
            "servicePlans": [
                {
                    "servicePlanId": "8c098270-9dd4-4350-9b30-ba4703f3b36b",
                    "servicePlanName": "ADALLOM_S_O365",
                    "provisioningStatus": "Success",
                    "appliesTo": "User"
                }
            ],
            "skuId": "c7df2760-2c81-4ef7-b578-5b5392b571df",
            "skuPartNumber": "ENTERPRISEPREMIUM",
            "appliesTo": "User"
        },
        {
            "capabilityStatus": "Suspended",
            "consumedUnits": 14,
            "id": "48a80680-7326-48cd-9935-b556b81d3a4e_d17b27af-3f49-4822-99f9-56a661538792",
            "prepaidUnits": {
                "enabled": 0,
                "suspended": 25,
                "warning": 0
            },
            "servicePlans": [
                {
                    "servicePlanId": "f9646fb2-e3b2-4309-95de-dc4833737456",
                    "servicePlanName": "CRMSTANDARD",
                    "provisioningStatus": "Disabled",
                    "appliesTo": "User"
                }
            ],
            "skuId": "d17b27af-3f49-4822-99f9-56a661538792",
            "skuPartNumber": "CRMSTANDARD",
            "appliesTo": "User"
        }
    ]
}

Your flow should now look like this. Run a test, to see if your data is parsed correctly.

There a couple of values that we need for our alerts:

skuPartNumber: the name of the SKU
enabled: this represents the total amount of installed licenses per SKU
consumedUnits: this is the number of licenses in use per SKU

In our next step, we are going to filter the array. I use this step to filter out licenses that have 0 active licenses. This is important because we are going to calculate the percentage later. If you leave them in, the math function, later on, will fail, as it cannot divide by zero. (neither can I 😉 )

You could also use this step, to filter for specific licenses. In some tenants, there are many different licenses, so you might want to monitor a specific license only.  To to that, just click " Edit in advanced mode" and add the following value: @and(greater(item()?['prepaidUnits']?['enabled'], 1),contains(item()?['skuPartNumber'], 'EMSPREMIUM'))
Replace 'EMSPREMIUM' with the SKU you're interested in.

Next, we are going to initialize four variables. First, we create the threshold variable to set the alert trigger. Next, we want to work with the total number of licenses and the active number of licenses, to calculate the percentage in use. Create the variables and select Float as the data type. In this example, I use 95 (%) as the threshold. You can adjust this to your own needs.

In the next step, create an Apply to each action and set the variables according to the figure below.

Make sure you select the body from the filtered array action in the output field, and not the unfiltered parsed data. 

In the calculate percentage variable we use an expression in combination with dynamic content. In this expression, I work with the math functions div and mul to calculate the percentage of the licenses in use. In my case, the expression is:

mul(div(variables('ActiveLicense'),variables('TotalLicense')),100)

This can be a little tricky to set up. Be aware that you use the correct name of the variables. Make sure that you understand what we are doing here: we divide the active licenses by the total licenses and multiply the outcome with 100 to get the percentage. So we are using a ‘div’ function within a ‘mul’ function.

You might see this, and think: this can be done in a much easier way! Please, let me know in the comments. I just figured out how to use math functions and dynamic content in a single expression, but I am aware that this might not be the most convenient method 😉

The last step is to create a condition with an if-statement. If the threshold is reached, take action. In this case, we send an email to Adele, the license admin of Contoso. If the outcome is false, no further action is taken.

The flow will look like this:

Let’s wrap up

Now you can go on and schedule this flow to run whenever you like. I would suggest running this flow daily. Depending on how you set it up, your email will look something like this:

You can also create other actions, like messages on Teams, service tickets, or text messages for example.

Variations

In this example, I use a percentage to be my threshold, but you can be creative, and adjust this to your needs. You might have just one or two licenses, and want to get alerted when you’ve got 10 licenses left. In that case, you adjust the threshold to 10 and then use the expression with the math function to calculate the free licenses:

sub(variables('TotalLicense'),variables('ActiveLicense'))

You can now use this outcome in your if-statement to trigger the alert when the licenses are less than your threshold.

Be creative!

Some useful reference

Reference guide to using functions in expressions for Azure Logic Apps and Power Automate

Power Automate documentation

Microsoft Graph REST API v1.0 reference

64 thoughts on “Use Power Automate or Logic Apps to keep an eye on your licenses”

  1. Pingback: Use Power Automate or Logic Apps to keep an eye on your licenses – 365ForAll

  2. Pingback: Use Power Automate as your Conditional Access Police Department - JanBakker.tech

      1. Hello Jan,
        I was able to follow up to the section “There a couple of values that we need for our alerts:”. Specifically, would you be kind enough to clarify and put more detailed steps and pictures for the steps related to “Next, we are going to initialize four variables. First, we create the threshold variable to set the alert trigger. Next, we want to work with the total number of licenses and the active number of licenses, to calculate the percentage in use. Create the variables and select Float as the data type. In this example, I use 95 (%) as the threshold.”? This would be greatly appreciated. Thank you Rafique

    1. This is great, but how do I modify this to only return SkuPartNumber of SPE_3 and SPE_5? We have many different license types, but are only concerned about Microsoft 365 Enterprise E3 and E5 license counts. THANKS!

      1. Yes, that is realy easy to do. Just edit the Fitler array step in advanced mode and change the value to @and(greater(item()?[‘prepaidUnits’]?[‘enabled’], 1),contains(item()?[‘skuPartNumber’], ‘EMSPREMIUM’)) where you replace ‘EMSPREMIUM’ with the SKU name you want to filter.

        So, what we are doing is to filter the array to only process the SKU’s with more than 1 license and with a specific name. You can add multiple SKU’s if needed, but in your case, it would look something like this: @and(greater(item()?[‘prepaidUnits’]?[‘enabled’], 1),contains(item()?[‘skuPartNumber’], ‘SPE_’))

    2. Thank you for the quick response, that worked! One more thing – the license accounts in the email notification for each SKU come out identical, even though the actual counts are different between SKUs. Is that because I need to define additional variables for each SKU? And is there a way to round off the percentage to a whole number?

      SPE_E5
      Below is an overview of the current licenses.
      Total Licenses:16363
      In Use: 16199
      98.9977388009534 % of the licenses are in use.

      SPE_E3
      Below is an overview of the current licenses.
      Total Licenses:16363
      In Use: 16199
      98.9977388009534 % of the licenses are in use.

      1. Make sure you selected the body of the filter array in the Apply to each action. From that point, all the steps need to be within the apply to each.
        I have not figured out to round the number. In your case, you could make 2 flows, each for 1 license. Instead of a percentage, you could just calculate the free licenses sub(variables(‘TotalLicense’),variables(‘ActiveLicense’)) like I described in the last section of the blog. Hope that helps.

        1. I have rounded the percentage to whole by setting up initial variable and set variable with the function
          “int(first(split(string(variables(‘Percentage’)),’.’)))”

          1. Have you been able to solve this? I really like this solution! But for some reasons the math functions doesn’t work for me, i tried both percentage and subscration. But the results doesn’t use the correct numbers. it only does the calculation once and uses this value for everything in the for each loop.

            The Set ActiveLicense and TotalLicense do work correctly, but the calculation won’t. Any ideas?

            Thanks!

          2. I can’t get past this error when testing powerflow: Insufficient privileges to complete the operation.
            My app reg has the following permissions:
            App – Group read all
            Delegated: Org read all, and user read
            Not sure what I’m missing

          3. how would i add multiple skus to the output
            @and(greater(item()?[‘prepaidUnits’]?[‘enabled’], 1),contains(item()?[‘skuPartNumber’], ‘EMSPREMIUM’)

            do use comma between each sku?

          4. Hi really cool this, it worked very simple.

            Is it also possible to get the names which are for example visible in the O365 admin Portal for the licenses in the email. So persons understand about which license it is, some persons dont know the skus.

            1. Hi mate, I like things simple and easy. So from the top of my head:

              1. Add a legend to your email to explain the SKU’s, like “RIGHTSMANAGEMENT” = “Azure Information Protection Plan 1” and so on.
              2. Add a Control->Switch action in your final step and use the skuPartNumber. Then make different switches for each SKU and hardcode the text in your mail. Don’t use the dynamic content.
              3. I’m sure there is something possible with the replace expression, but I have no experience with that.

              Hope that helped you out!

              1. How would one go about to create that switch? The reports works great and I greatly appreciate the guide you’ve put together. But I have a certain feeling that some people at my workplace will just get confused when they see names like “ENTERPRISEPREMIUM” or “STANDARDWOFFPACK_FACULTY”

                1. Hi Andrew, I fully agree. Adding a more user-friendly description would make much sense. As stated before, you can add a legend to your email explaining the different SKUs. That is the easiest approach. Building the switch will take more effort, both to build and maintain. It’s up to you.

              2. Pingback: Office 365 License Alerts – Send alerts to a Teams channel with adaptive cards – Whack Whack

              3. Pingback: License on-demand with Power Automate and Azure AD - JanBakker.tech

              4. Hi Jan, what power automate license is required for this? I have M365 E5 but I think that is more targeted at user area (i.e. users automating things around their service instances like EOL, Sharepoint etc.)

              5. Great Article,

                How would you handle reporting on multiple product? I want to monitor for EMS, ENTERPRISEPACK and ENTERPRISEPREMIUM ?
                do you need to create multiple Array?

                1. Nice tut. ty.
                  Any idea on how to look for when a spesific lisense is consumed an then trigger somtehing?
                  Can’t seem to find if/where to look for when a new license is applied to a user.

                  Thanks again.

                2. Would be good if you can clarify the following.

                  1. How do you list multiple SKU’s in the filter array, e.g I want to see EMS and AZURE_AD_P2, how is this achieved?
                  2. The calculation provided doesn’t work, I just get 0 for percentage when using:

                  mul(div(variables(‘ActiveLicence’), variables(‘TotalLicence’)), 100)

                3. Pingback: One year anniversary JanBakker.tech - JanBakker.tech

                4. Pingback: One year anniversary JanBakker.tech - Tech Daily Chronicle

                5. My issue is after Parse JSON step, I cannot find “value” and “enabled” from dynamic expression under “Filter Array” field. Hence, I’m unable to proceed further

                  Any help would be greatly appreciated.

                6. Hi I setup the Logic app, but I am getting very strange results in my email..
                  In all mails the subject is different so it is scanning al licenses we have.
                  But when I look at the body of the mail I see a lot of same Total licenses and Assigned licenses which dont correspond with the licenses in the cloud.
                  Also when I run the logic app for a second time I get totally different results in the mail..
                  It seems like it is unable to set the right variables in the right mails or it seems to be dropping the variable update?
                  Please assist me!

                  1. Hi there! Based on what I read, this is caused by concurrency control. When you enabled this, data can be mixed up. Please check.

                  2. enabled, suspended and warning are always 0, might be the JSON parsing

                    How do you read these values in json format ?

                    “prepaidUnits” : {
                    “enabled”: 0,
                    “suspended”: 0,
                    “warning”: 25
                    }

                  3. I’m Getting error in the final step. Please Advise
                    Send an email notification (V3)
                    {
                    “statusCode”: 401,
                    “headers”: {
                    “Date”: “Wed, 29 Sep 2021 17:03:35 GMT”,
                    “Content-Length”: “229”
                    },
                    “body”: {
                    “$content-type”: “application/octet-stream”,

                  4. Hi, I want to only filter E3 licenses but when I use the following expression:
                    @and(greater(item()?[prepaidUnits’]?[enabled’], 1),contains(item()?[‘skuPartNumber’], ‘ENTERPRISEPACK’))

                    it says it is an invalid expression. I copied your exact expression with the exception of modifying EMSPREMIUM to ENTERPRISEPACK.

                      1. Hi, I used this example as inspiration, but I went about it in a slightly different way… – I don’t have any values going into variables, and I don’t use a loop (yet) – my initial aim was just a pretty license report without the logic to compare to a threshold (I’ll add that later). In my flow I take the output of the filtered array straight into a Create HTML table action. It works well, I use custom column headers, and a combination of dynamic content and expressions in the column values.. all my columns work perfectly except for the percentage… – it shows 100% correctly where there are n of n licenses used, but for any percentage under 100, it shows as zero… is this because I’m not using variables typed as float? My expression to calculate the value is,

                        mul(div(item()?[‘consumedunits’],item()?[‘prepaidunits’]?[‘enabled’]),100)

                        and my table output comes out like this:

                        SKUPartNumber SKUName TotalUnits Consumed Available PercentUsed
                        VISIOCLIENT Visio Plan 2 2 2 0 100
                        EMSPREMIUM Microsoft 365 E3 60 55 5 0

                        (I hope the layout comes out OK to keep it all in aligned columns)… So you see the % value for Visio is correct at 100%, but the E3 SKU should be 91.6% but shows as zero…

                        Incidentally, I get the friendly SKUName with an expression with a bunch of nested replace functions placed right in the Create HTML Table’s value column, my expression is this:

                        replace(replace(replace(replace(replace(replace(replace(item()?[‘skuPartNumber’],’VISIOCLIENT’,’Visio Plan 2′),’EMSPREMIUM’,’Microsoft 365 E3′),’DYN365_BUSCENTRAL_ESSENTIAL’,’Dynamics 365 Business Central Essentials’),’IDENTITY_THREAT_PROTECTION’,’Microsoft 365 E5 Security’),’MCOMEETADV’,’Microsoft 365 Audio Conferencing’),’SPE_E3′,’Microsoft 365 E3′),’INFORMATION_PROTECTION_COMPLIANCE’,’Microsoft 365 E5 Compliance’)

                        It obviously only caters for the SKU’s for which I define a replacement string, but that works well enough in my environment, and if we do ever get other license SKUs I only have to make a quick modification to this expression to add the necessary replacement string. (this isn’t something that happens too often).

                      2. Hello,
                        Could someone please help me understand why in Calculate Percentage in use step i can’t use the mul(div(variables(‘ActiveLicense’),variables(‘TotalLicense’)),100) part. It keeps telling me that value is incorrect.

                        Thank you

                        1. Nevermind. I had to use expression part. I had my screen halved and didn’t see it 🙂 Thank you for the great guide by the way. I will be forever thankful to you!

                        2. Thanks, very helpful blog! I build it in a logic App with a Managed identity.
                          The only problem I ran in to, that took me a while to figure out was the loop over the list with licenses. Somehow it irregular came up with the wrong values in the mail. Some googling made me try change a setting in the For Each loop: Concurrency Control switch on and slide the value all the way down to 1.
                          After changing this setting it works perfectly.
                          Maybe this helps for others who run into the same problem.
                          The only thing still need to figure out is how to mail out with the managed identity.

                          1. Hi Kees, that is indeed something I’ve ran into as well.

                            I think it’s time for a V2 of this post 😉
                            I’m happy to see what you’ve built so far.

                        3. sankarasubramanian parameswaran

                          error reported when i use this function.
                          1
                          sub(variables(‘TotalLicense’),variables(‘ActiveLicense’))
                          or Mul …Error related to Total license need to be intialized first

                        4. Jan – Thank you, this worked brilliantly!

                          Since others asked… To monitor multiple products, you will need to create two filter arrays.
                          Filter array #1 from Parse JSON value:
                          @or
                          (
                          contains(item()?[‘skuPartNumber’], ‘SPE_E5’),
                          contains(item()?[‘skuPartNumber’], ‘EMSPREMIUM’),
                          contains(item()?[‘skuPartNumber’], ‘MEETING_ROOM’),
                          contains(item()?[‘skuPartNumber’], ‘VISIOCLIENT’),
                          contains(item()?[‘skuPartNumber’], ‘DYN365_ENTERPRISE_SALES’),
                          contains(item()?[‘skuPartNumber’], ‘D365_SALES_PRO’),
                          contains(item()?[‘skuPartNumber’], ‘DYN365_ENTERPRISE_TEAM_MEMBERS’)
                          )

                          Filter array #2 from array #1 body:
                          @and(greater(item()?[‘prepaidUnits’]?[‘enabled’], 1))

                          Hope this helps others.

                        5. Licenses which are fully exhausted and available = 0, the enabled response does not show the total count value, instead it displays the total number of that license in warnings. how do i correct that ?

                        6. Hi!

                          A brilliant guide which will most certainly help out IT Support team to better manage licences.

                          One question, is it possible to generate the email based on a number of licences remaining rather than a percentage of remaining licences?

                          For example, trigger the alert email when we fall below a threshold of 10 available licences.

                          Apologies if this is a silly question, this is the first time I’ve touched Power Automate.

                          Kind regards,
                          Ash

                          1. Sure, that can be done as well.
                            Instead of calculating the percentage, just subtract the used licenses from the total amount, and change your threshold to 10.

                            sub(variables(‘ActiveLicense’),variables(‘TotalLicense’)) should do the trick.

                        7. Hello All,

                          Very new to PA, i am stuck on Parse JSON step, what i mention in the schema.
                          Do i just copy paste(after editing Account details) the query mentions above.

                        8. I am stuck on Calculate percentage set variable in foreach action.
                          I am not sure how to set the value to mul(…), it does not show up in Expression.

                        9. I am stuck on Calculate percentage set variable in foreach action.
                          I am not sure how to set the value to mul(…), it does not show up in Expression.

Leave a Reply

Your email address will not be published.