Monitor your Microsoft 365 licenses with Logic Apps

Today a blog post in which we use a Logic Apps flow to monitor our Microsoft 365 licenses, such as Office 365 and Intune licenses. With this flow, we’ll receive an email when the number of available licenses gets below a certain minimum number.

The solution

The solution is pretty simple and mostly relies on Graph API queries. Via Grap API we can query all our Microsoft 365 licenses. The information we get back holds the number of enabled and consumed licenses, with which we can calculate the number of available licenses. I also added an additional part in the flow, to get notified on an expired license, which is still assigned to user accounts and still might be needed although the license is expired.

This is what the complete flow looks like.

The flow is also available on my GitHub repository as an ARM template.
Luise Freese contributed to the solution and now you can also deploy it with Azure Bicep and use an Azure Managed Identity instead of a client secret (not described in this article, but available on GitHub).

The requirements

To get this Logic Apps flow up and running we have a few requirements.

Via an HTTP action, we’re going to query Graph API for the information we need. This HTTP action uses an Azure App Registration, which is our first requirement. The App Registration is used for authentication in the HTTP actions and also for granting the required permissions.
The minimum required (Application) permission for this Logic App is shown below:

To secure the HTTP action I use Azure Key Vault, which is described in this previous post. The Key Vault holds the client secret and keeps this secure in the flow. I also added the tenant id and client id to the vault, so I only have to query the Key vault for this information.
In every HTTP action, the tenant and client id need to be added. By storing the information in a variable or in a Key Vault, we don’t have to copy/ paste the ids in every HTTP action.

Depending on how you want to receive the notifications, permissions on a (shared) mailbox or a Teams webhook connector are needed.

Setup the Logic Apps flow

Let’s configure the Logic Apps flow!

Sign in to the Azure portal and open the Logic App service. I created a blank Logic App of type Consumption.

When the creation of the Logic App is finished, open the flow. A few templates are shown, choose Recurrence.

The Logic App designer is opened and the recurrence trigger is added. Choose the recurrence settings of your choice; once a day, once a week.

The first action we’re going to add is the Get secret action, which is an Azure Key Vault action. With this action, we retrieve the tenant id, which we use in the HTTP action.
Search on Azure Key Vault and select Get secret.

Enter the name of the Vault and select your tenant. Next, click Sign in to authenticate to the Key Vault.

Choose tenant-id from the drop-down list.

Repeat the Key Vault steps to also retrieve the client id and client secret.

The next step we’re going to add is an HTTP Action, to query Graph for the licenses.

Choose GET as Method.
As URI enter:

Choose Add new parameter and check Authentication. Select Active Directory OAuth as authentication type.
As tenant, client id, and secret, we add the Dynamic content Value. Make sure you pick the value from the correct Key Vault action.
Enter as Audience.

This is our HTTP action.

To use the information we received (by executing this HTTP action) in the upcoming actions, we need to parse the received information with a Parse JSON action.

To fill the schema with the correct information, we can add an example payload. The information to use as an example payload can be ‘generated’ by running the flow. Save the flow and hit Run trigger to start the flow.

Open the Runs history from the flow and open the HTTP action. Copy the body.

Add a Parse JSON action, which is a Data operation action.
Add Body of the HTTP action in the Content field.
Click Use sample payload in the Parse JSON action, past the information which we copied from the runs history in the new pop-up, and click Done.

This is our Parse JSON action.

The next action is an HTTP action. With this action, we run a query to retrieve information for a single license, for which we use the ID of the license. Because we retrieved multiple license IDs, the HTTP action is automatically placed in a For each action, as soon as we add the Dynamic content ID (from the Parse JSON action).

I query the licenses one by one, although we already have all the information from the first HTTP action, to make sure the output from all the licenses doesn’t get mixed and so I’m sure the numbers I use later in the actions are correct.

Choose GET as Method.
As URI enter:[ID]

Replace [ID] with the ID found via Dynamic content.

Further, fill in the Audience and values for tenant id, client id, and client secret.

We need to parse the output from the HTTP action again with a Parse JSON action.

Next, I wanted to use a Variable action to calculate the number of available licenses and use that variable in the coming actions. Although this calculation itself within a variable works fine, for some reason the usage of the variable in the upcoming Control action gave me inconsistent results. That’s why I don’t use a variable, but I do the calculation in the Control action itself.

Add a Condition action, which is a Control action.

I want to make sure that only the Enabled licenses are used in the upcoming action, that’s why I use a condition Enabled is greater than 0. That’s because we can also have a number of enabled licenses, although the license itself has another status (warning for example), which I process in another part of this flow.
Enabled is used from the latest Parse JSON action.

We are going to subtract the Enabled and consumptedUnits numbers from the latest Parse JSON action, the output is our number of available licenses.

Click Add and add a new row.
In the left field, we are going to add the subtraction. Open the Expression tab and enter sub().

Open the Dynamic content tab, make sure the cursor is located between the parentheses (), and choose Enabled from the list (from the latest Parse JSON action).
Place the cursor between the comma and closing parenthesis and add consumedUnits.
Click OK.

Choose is less than and in the right field enter the number under which you want to receive a warning.

You can also copy and paste the below as Expression, instead of adding the above subtraction manually. But make sure you replace Parse_JSON_GET_one_Sku_enabled (twice) with the name of your own Parse JSON action, and replace the spaces with a hyphen.


Under True, we can add our action to get notified. In my case, I used an action to send an email from a shared mailbox.
We can use the information from the latest Parse JSON action and the subtraction, to put this information in the notification.

The above actions are enough to get notified when you’re running out of licenses. But I’ve also seen licenses that are already expired (which get a status of Warning instead of Enabled) but are still assigned and used. To get also notified of these licenses, we can add a parallel branch in our flow, which runs next to the above created flow.

To create this parallel branch, hit the plus sign between the first Parse JSON action and For each action and choose Add a parallel branch.

Like in the other branch, we are querying Graph for the licenses with the license ID, with an HTTP action.

And we also need to add a Parse JSON action.

Next, we need to add a Condition action.

I want to get notified when the number of licenses with a status of Warning is greater than 0 and if we also have consumed licenses. Therefore we add Enabled and consumedUnits in the left field (add a second row).

Under True, we can add our notification.

That,s it! Our license monitoring system is ready!


  1. Hello, very promising app but i am getting this error:

    “InvalidTemplate. Unable to process template language expressions for action ‘Condition_2’ at line ‘0’ and column ‘0’: ‘The template language function ‘greater’ expects all of its parameters to be either integer or decimal numbers. Found invalid parameter types: ‘Null’.’.”

    Why i am getting Null and not an integer?

  2. Is there an easy way to have this (and your other logic apps) send a singular email containing all the info vs an individual email for each license? Looking at posting this in teams vs email, but either way getting multiple emails is inefficient but i am struggling to find a way to group all the info and send as one message.

  3. Hello,

    Is there a way to read the license expire date? Like send mail 1 week before license will expire and how many there are not assignd and assigned.

    Is that possible to create? Rest is working fine for me.

Leave a Reply

Your email address will not be published.