Skip to content
The Lazy Administrator
  • Home
  • Disclaimer
  • Contact
  • About Me
  • Search Icon

The Lazy Administrator

Finding ways to do the most work with the least effort possible

Building Event-Driven Automations in Microsoft 365 Using Graph Subscriptions

Building Event-Driven Automations in Microsoft 365 Using Graph Subscriptions

November 3, 2025 Brad Wyatt Comments 0 Comment

Table of Contents

  • Objective
    • Pre-Requisites
  • Resolution
    • Entra ID
      • Create the Application Registration
      • Assign API Permissions
      • Create Client Secret
      • Create Entra ID Group
    • Azure Function
      • Create an Azure Function App
      • Add Environment Variables
      • Modify the Function
    • Subscription
      • Connect to Graph API
      • Create Subscription Notification

Objective

This post demonstrates how to use Microsoft Graph Subscriptions to create event-driven automation in Microsoft 365. Graph Subscriptions allow applications to receive real-time notifications when specific changes occur within Microsoft 365 resources such as users, groups, or mailboxes, without the need for constant polling. By subscribing to these change events, we can build responsive workflows that automatically react to updates like group membership changes and trigger downstream actions in Azure Functions or Automation Accounts.

In this article, I will create a Microsoft Graph Subscription that listens for changes to a specific group’s membership. When a user is added or removed, it triggers an Azure Function that identifies the user, looks them up in Microsoft 365, and then adds or removes them from the Proofpoint portal accordingly.

A subscription lifespan is dependent on the resource with some as short as 1 hour. Refer to the subscription lifetime documentation for more information.

Pre-Requisites

  1. You need to be able to create Application Registrations in the Entra ID Portal and assign them API permissions, some of which may require admin consent. The following table lays out the type of permission you need for each supported resource.
    1. In our example, we will be using the group resource type and will need Group.Read.All
  2. You need to be able to create or have an HTTP trigger Azure Function

Resolution

Entra ID

Create the Application Registration

  1. Navigate to the Azure Portal and then go to Microsoft Entra ID
  2. In the left pane, click App registrations and then click “+ New registration”

3. Give your app registration a name. For support account types keep it at “Accounts in this organizational directory only” and you can keep the Redirect URI empty. In my example, I named my app GroupChangeSubscriptionListener. When finished, click “Register”.

4. When the app registration is complete, note the ”Application (client) ID” and the Directory (tenant) ID for later.

Assign API Permissions

  1. With our newly created app registration, we now need to add API permissions to it. For our example, we need to grant it Group.Read.All. In the left pane, click “API permissions”

2. In the “API permissions” pane, click “+ Add a permission” button

3. Since we need to add Group.Read.All we need to click Microsoft Graph > Application permissions > Group.Read.All and then click “Add permissions”. For my personal automation I also need User.ReadBasic.All to look up the users in Entra ID but you may not need it.

4. Because this permission requires admin consent, we see the the status for the permission is “Not granted”. Click the “Grant admin consent button” and then click “Yes” on the confirmation prompt.

Create Client Secret

  1. Next, we need to generate a new client secret for our App Registration so we can connect to the Microsoft Graph API with it and create our new subscription. In the left pane click “Certificates & secrets” and then click “+ New client secret”

2. Give your client secret a good name and expiration date then click Add.

3. Copy down the secret value for later.

Create Entra ID Group

  1. Next, we need to create a new Entra ID group that we will use to monitor membership changes.
    Note: Subscription notifications will fire for dynamic group membership changes as well as static memberships.
  2. Go to Groups and then click “New Group”

3. Give the group a proper name and then click “Create”

4. Go to the groups detail page and copy the Object Id for later.

Azure Function

Create an Azure Function App

  1. Next, we need to create an Azure Function. The Azure Function is what will be receiving our subscription notification, parsing the information, and performing any further actions that you specify.
  2. In the Azure Portal, go to Function App and click “+ Create”.

3. Select a hosting plan and then click Select

4. Configure you function app to your liking. Set the runtime to PowerShell Core.

5. Next, create a new Function that is an HTTP trigger. I named my function, “GroupMembershipChange”

Add Environment Variables

  1. In the left pane, select “Environment variables”

2. The first variable we are going to add is called clientState. For the value, I like to think of a 3 word string with a trailing number. clientState is to confirm that change notifications you receive originate from Microsoft Graph. For this reason, the value of the property should remain secret and known only to your application and the Microsoft Graph service. Note the value for later as well.

Modify the Function

Next, we need to modify the function code itself to properly handle and parse the notification body.

  1. In the run.ps1 file of the function, replace everything with the code below. If you are wanting to perform your own actions, copy the code below and add your code to the TODO line:

GitHub Link

If you are also doing the same Proofpoint automation, you can grab my function code below which will look up each user by their ID, then take the users UserPrincipalName and see if the user is already in ProofPoint prior to add or removing them:

GitHub Link

2. You may be wondering what the POST looks like from the subscription when users are either added or removed from the group. Below you can view and example body. Disregard the clientState value, we will go over that in a few steps.

a. Added

{
  "value": [
    {
      "changeType": "updated",
      "clientState": "FROSTY-offender-paulette",
      "resource": "Groups/7caec37e-d035-4789-af30-a7d5f7f8e24b",
      "resourceData": {
        "@odata.type": "#Microsoft.Graph.Group",
        "@odata.id": "Groups/7caec37e-d035-4789-af30-a7d5f7f8e24b",
        "id": "7caec37e-d035-4789-af30-a7d5f7f8e24b",
        "organizationId": "6438b2c9-54e9-4fce-9851-f00c24b5dc1f",
        "members@delta": [
          {
            "id": "d4cd3c57-2998-48d8-8d0b-63b92def0894"
          },
          {
            "id": "0171d611-cc0a-41a4-b4c9-eb55a9cfa8aa"
          },
          {
            "id": "ac3f067f-b3be-40b7-bf6c-8d2e6a1b2343"
          }
        ]
      },
      "subscriptionExpirationDateTime": "2025-10-29T18:23:45.9356913+00:00",
      "subscriptionId": "db1c6f03-1968-4f2e-a5fe-f99d52993082",
      "tenantId": "6438b2c9-54e9-4fce-9851-f00c24b5dc1f"
    }
  ]
}

b. Removed

{
  "value": [
    {
      "changeType": "updated",
      "clientState": "FROSTY-offender-paulette",
      "resource": "Groups/7caec37e-d035-4789-af30-a7d5f7f8e24b",
      "resourceData": {
        "@odata.type": "#Microsoft.Graph.Group",
        "@odata.id": "Groups/7caec37e-d035-4789-af30-a7d5f7f8e24b",
        "id": "7caec37e-d035-4789-af30-a7d5f7f8e24b",
        "organizationId": "6438b2c9-54e9-4fce-9851-f00c24b5dc1f",
        "members@delta": [
          {
            "id": "d4cd3c57-2998-48d8-8d0b-63b92def0894",
            "@removed": "deleted"
          },
          {
            "id": "0171d611-cc0a-41a4-b4c9-eb55a9cfa8aa",
            "@removed": "deleted"
          },
          {
            "id": "ac3f067f-b3be-40b7-bf6c-8d2e6a1b2343",
            "@removed": "deleted"
          }
        ]
      },
      "subscriptionExpirationDateTime": "2025-10-29T18:23:45.9356913+00:00",
      "subscriptionId": "db1c6f03-1968-4f2e-a5fe-f99d52993082",
      "tenantId": "6438b2c9-54e9-4fce-9851-f00c24b5dc1f"
    }
  ]
}

3. You may have noticed that the Azure Function contains logic at the top to respond to validation requests. Subscription notification endpoint (or notificationUrl) must be able to properly respond to the validation attempt, otherwise when creating the subscription you will get a 400 Bad Request response. You can read about this here.

4. Lastly, we need to grab the functions URL. Click “Get function URL” and then copy the default url. Note it for later.

Subscription

Connect to Graph API

  1. Using any REST client you want, including something like Invoke-RestMethod or curl, we will now connect to the Microsoft Graph API to get an access token. I will be using REST Client VS Code extension in my examples. (You will see variables formatted as {{variable}} and the value has been added to my VS Code’s settings.json file.)
  2. To get an access token we need to know our tenant ID, client ID and client secret. After providing those values we can perform a POST to https://login.microsoftonline.com/{{tenantId}}/oauth2/v2.0/token with the body containing the rest of the information: client_id={{clientId}}&scope=https://graph.microsoft.com/.default&client_secret={{clientSecret}}&grant_type=client_credentials My .http file is as follows:
POST https://login.microsoftonline.com/{{tenantId}}/oauth2/v2.0/token

Content-Type: application/x-www-form-urlencoded

client_id={{clientId}}&scope=https://graph.microsoft.com/.default&client_secret={{clientSecret}}&grant_type=client_credentials

3. In the response we get an access_token. We will use this to perform our next steps. In my case, I will save this to my settings.json file as a variable called access_token

Create Subscription Notification

  1. Our final step is to create the subscription using the Graph API. Since we have our access token, we will use that to create the new subscription.
  2. Next, we will make a POST request to /subscriptions to create the subscription but first we need to paste our Functions URL as the notificationUrl, the resource is groups/GROUPID/members replacing “GROUPID” with the objectId of our group, and the clientState is the value we created earlier that we check for to ensure that any requests coming to our function are from this subscription. The expirationDateTime needs to be a date in the future but not exceeding the limits we looked at earlier.
POST {{baseUrl}}/subscriptions
Content-type: application/json
Authorization: Bearer {{access_token}}

{
   "changeType": "updated",
   "notificationUrl": "https://func-subscriptionnotifications-g8h9fnh4fbahdycp.northcentralus-01.azurewebsites.net/api/GroupMembershipChange?code=zpzkGlybkK_ntKLliqHFmJr8GvVMpS9196Bbiy_x1aVlAzFu5oBrNw==",
   "resource": "groups/c0880d17-e166-4bad-ae0b-8c1f05e3d7be/members",
   "expirationDateTime":"2025-10-29T18:23:45.9356913Z",
   "latestSupportedTlsVersion": "v1_2",
   "clientState": "CHICAGO-lake-SALUKI8"
}

3. Now you may notice that we do not specify the app registration anywhere at all. This is because Microsoft Graph “knows” which app is creating the subscription from the OAuth 2.0 access token you send in the Authorization header. The token contains the app’s client ID, which Graph records as subscription.applicationId.

4. After sending the POST I get the following response:

 {
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity",
  "id": "7ac32162-a852-436f-9c49-f67cc4a71a6a",
  "resource": "groups/c0880d17-e166-4bad-ae0b-8c1f05e3d7be/members",
  "applicationId": "e75d0642-26c2-4270-8da3-e5a754754cad",
  "changeType": "updated",
  "clientState": "CHICAGO-lake-SALUKI8",
  "notificationUrl": "https://func-subscriptionnotifications-g8h9fnh4fbahdycp.northcentralus-01.azurewebsites.net/api/GroupMembershipChange?code=zpzkGlybkK_ntKLliqHFmJr8GvVMpS9196Bbiy_x1aVlAzFu5oBrNw==",
  "notificationQueryOptions": null,
  "lifecycleNotificationUrl": null,
  "expirationDateTime": "2025-10-29T18:23:45.9356913Z",
  "creatorId": "af49439d-6f5e-4d9d-b05f-a7ff076b8dfa",
  "includeResourceData": null,
  "latestSupportedTlsVersion": "v1_2",
  "encryptionCertificate": null,
  "encryptionCertificateId": null,
  "notificationUrlAppId": null
}

5. Now if you remember, the subscriptions have a short TTL and will expire rather quickly. Typically I have another app registration with subscription.read.all permission to read our subscriptions and their expiration dates, and then I will extend it out a day or so.

PATCH {{baseUrl}}/subscriptions/9d5c9a8d-3fda-462c-a81f-118967508032
Content-type: application/json
Authorization: Bearer {{access_token}}

{
   "expirationDateTime":"2025-10-29T18:23:45.9356913Z",
}
Brad Wyatt
Brad Wyatt

My name is Bradley Wyatt; I am a 5x Microsoft Most Valuable Professional (MVP) in Microsoft Azure and Microsoft 365. I have given talks at many different conferences, user groups, and companies throughout the United States, ranging from PowerShell to DevOps Security best practices, and I am the 2022 North American Outstanding Contribution to the Microsoft Community winner.


Azure, Graph, PowerShell
Automation, Azure, Graph, Office 365, PowerShell

Post navigation

PREVIOUS
Auto Deploy Progressive Web Applications (PWA) using Intune or PowerShell

Leave a Reply Cancel reply

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

Subscribe

Email


Categories

  • Active Directory (8)
  • AI (3)
  • API (1)
  • AutoPilot (2)
  • Azure (16)
  • Bicep (4)
  • Connectwise (1)
  • Defender for Cloud Apps (1)
  • Delegated Admin (1)
  • DevOps (6)
  • Graph (7)
  • Intune (16)
  • LabTech (1)
  • Microsoft Teams (6)
  • Office 365 (19)
  • Permissions (2)
  • PowerShell (52)
  • Security (1)
  • SharePoint (3)
  • Skype for Business (1)
  • Terraform (1)
  • Uncategorized (2)
  • Yammer (1)

Recent Comments

  • fabio on Set-ADUser: Dealing with Null Values when Importing a CSV; Working with Parameters and Properties that don’t Accept Empty Strings
  • Dominik on Auto Deploy Progressive Web Applications (PWA) using Intune or PowerShell
  • Darren Heath on Get a New Computer’s Auto Pilot Hash Without Going Through the Out of Box Experience (OOBE)
  • Ryan on Auto Deploy Progressive Web Applications (PWA) using Intune or PowerShell
  • 91 Club Lottery on Get a New Computer’s Auto Pilot Hash Without Going Through the Out of Box Experience (OOBE)

1,814,494 People Reached

© 2025   All Rights Reserved.