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

80 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

      2. Hello Jan, First of all many thanks for the very useful tutorial.
        May I know (once you get a chance) what the best approach is if I need to set up multiple alerts for around ten different SKU’s or more with various threshold limits?
        Thanks in advance.

  3. 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_’))

  4. 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’)),’.’)))”

    2. 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!

  5. 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

  6. 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?

  7. 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.

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

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

  10. 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.)

  11. 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?

  12. 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.

  13. 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)

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

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

  16. 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.

  17. 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.

  18. 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
    }

  19. 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”,

  20. 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.

  21. 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).

  22. 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!

  23. 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.

  24. 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

  25. 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.

    1. for your 2 filter arrays could you provide step by step on how to set this up please?

      I created a new filter array ‘From: value’ with your
      @or
      (
      contains(item()?[‘skuPartNumber’], ‘SPE_E3’),
      contains(item()?[‘skuPartNumber’], ‘SPE_F1’),
      contains(item()?[‘skuPartNumber’], ‘STANDARDPACK’),
      contains(item()?[‘skuPartNumber’], ‘POWERAPPS_PER_APP’),
      contains(item()?[‘skuPartNumber’], ‘EXCHANGEENTERPRISE’),
      contains(item()?[‘skuPartNumber’], ‘EXCHANGEARCHIVE_ADDON’),
      contains(item()?[‘skuPartNumber’], ‘MCOMEETADV’)
      )
      and it fails

  26. 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 ?

  27. 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.

  28. 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.

  29. 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.

  30. 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.

  31. Hi – thanks for the effort here, but I ran this completely vanilla as described and while being a novice to power automate it took me a while to fill in some missing pieces, I now have it set exactly as described here.

    However after test running it the script runs indefinitely and gets stuck at the “apply to each” step and will never finish running. I can’t figure out anywhere where it might spell out exactly what its stuck on but unfortunately I can’t get past this issue.

    1. It depends on your licenses, but it should’nt take long. Check the logs to see what took so long, and work from there.

  32. I’m stuck on how to create the ‘Apply to each’ ‘Calculate percentage in use’
    ake 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)

    How do you create this? in your screen shot looks like ‘fx’ and not able to do that.

  33. You a variable called ‘calculate percentage in use’ in the Apply to each action with mul(div(variables(‘ActiveLicense’),variables(‘TotalLicense’)),100)

    However in the image below where you state ‘The flow will look like this: the ‘calculate percentage in use’ in the Apply to each action with mul(div(variables(‘ActiveLicense’),variables(‘TotalLicense’)),100) is not visible?

    Please can you explain

  34. Hi great article.
    Im very new to Power Automate and im just abit stuck at the Parse JSON thing.
    I have run the get request fine and it outputs. Do i need to just copy and paste that for the schema. When I do this it errors out with “Fix invalid expressions for the input parameters of operation ‘Parse JSON’.
    Or do I just copy my output and put it into the output in the article?

    1. Almost there. Just copy the output of the HTTP request, and use it to generate the schema from sample. So don’t past the out in the schema itself, but pick “generate from sample”

      1. Thanks for this.
        It seems so easy when its put like that.
        Just doing the rest and hope to see it output soon.
        I did see some one ask about the total rather than percentage so ill look at that.

  35. Hi

    Im relatively new to all this as well.

    Im have followed your guide and it saves fine with no errors but when I run the flow it says error at hte filter array.
    The “from” property value in the “query” action inputs is of “null”. The value must be an array.

    The parse JSON has a green tick and I copied the out put when I set it up and pasted it into the generate and there were no errors in there.

  36. Hi Jan, I SKU that I get from my HTTP reqeust is not 100% match with my current license that I have in Admin Center. For example, I would like to monitor only 2 types of licenses i.e Exchange Online P1 and Exchange Online P2, these licenses were not listed in the output. When compared I could find few licenses that matched with admin portal.
    May I know why?

    Anticipating your response.

    Thanks in advance!!

  37. First and foremost, thank you for putting this together! Just for future reference for anyone trying this, I had an issue with the email data for “TotalLicense”, “ActiveLicense”, and “Percentage”. They were always the same number. The “SkuPartnumber” was correct and rotating through the licenses. I verified I was using the FilterArray variables as noted in the guide. Reading through the comments, I also tried concurrency setting to 1; I tried turning it off as well. One thing that was different when I did this was when creating the Send an email notification (V3), it automatically put the Apply to each in after adding the FilterArray body.
    To get the correct numbers with the licenses in the emails I had to use (I didn’t use the percentage in the email):
    In Use: @{items(‘Apply_to_each_2’)?[‘consumedUnits’]}
    Total license: @{items(‘Apply_to_each_2’)?[‘prepaidUnits’]?[‘enabled’]}

  38. thanks for the great article.
    one Q, how can I send all the license total and free as a table to only one e-mail. (in addition to the alert threshold for each that is already described in your article)

Leave a Reply

Your email address will not be published. Required fields are marked *