According to Microsoft, getting incremental changes for Azure AD groups is done using Delta Queries of the Microsoft Graph API.
I wanted to build a solution that could get incremental changes for a group, process the result and save a “state” to get only the new changes the next time. I also wanted to build a generic solution to be able to monitor multiple groups at once and build it serverless. So I came with this implementation:

First Function: Timer Trigger
The first function is used to schedule the mechanism, so for this implementation I used a Timer Trigger Function written with PowerShell.
The code of this function is very simple and is just a HTTP Request to the second function. The advantage of this is that it can make multiple Http calls to the second function to trigger the delta query mechanism on multiple groups at the same time
$groupId = "[ID of the group to monitor]"
$queueName = "[Name of the queue to send the results to]"
$url = "[Url of the second Azure Function]&groupid={0}&queuename={1}" -f $groupId,$queueName
Invoke-RestMethod -method Get -uri $url -headers @{"content-type"="application/json"}
Second Function: Http Trigger
The second function is the one making the delta query call for the Azure AD Group ID passed as a parameter then sends the results to the queue which name is also passed as a parameter.
According to the documentation, the delta query returns the results of the changes that appears in the group as well as the next query that should be called the next time. So in order to get incremental changes, we have to save the next query string somewhere so that the next call would contains only the new changes done to the group. (Add member, remove member, change name…).
As an Azure function uses the File Storage system of the storage account linked to it, I chose to save this value (the next query) in a file. The content of this file being replaced each time with the next query to call. Another place could have been a Table, or a blob, but I wanted to keep it simple and in the same place as the function.
The code of this function looks like that:
$baseQuery = "https://graph.microsoft.com/beta/groups/delta?&filter=id eq '{0}'&`$expand=Members"
# get the parameters from the Http Request
if($req_query_queuename){
$queueName = $req_query_queuename
}
else{
$queueName = ""
}
if($req_query_groupid){
$groupid = $req_query_groupid
#Try to load local file with DeltaLink
$filePath = "$PSScriptRoot\deltaLink_{0}.json" -f $groupid
if(Test-Path $filePath){
$deltaLinkFile = Get-Content $filePath -Raw -Encoding UTF8 | ConvertFrom-Json
if($deltaLinkFile.NextDeltaLink){
$query = $deltaLinkFile.NextDeltaLink
}
}
else{
$query = $baseQuery -f $groupid
}
}
else{
$query = $null
}
write-output "Query = $query"
if ($query -and $queueName -ne "")
{
$tenantId = $env:TENANTID
$clientId = $env:APPCLIENTID
$clientSecret = $env:APPCLIENTSECRET
$resourceUrl = "https://graph.microsoft.com"
# function to get an access token from Azure AD (not explained in this article: see https://docs.microsoft.com/en-us/graph/auth-v2-service#4-get-an-access-token)
$accessToken = Get-GraphAccessToken -AppID $clientId -AppSecret $clientSecret -TokenAuthURI "https://login.microsoftonline.com/$tenantId/oauth2/token"
$nextDeltaLink = ""
$result = @()
if($accessToken){
# send the request (function explained below)
$resultQuery = Send-GraphApiRequest -Uri $query -accessToken $accessToken -method Get
$result = $resultQuery.Results
$nextDeltaLink = $resultQuery.NextDeltaLink
}
Write-Output $resultQuery
if($nextDeltaLink){
Write-Output "Saving NextDeltaLink"
$dataToExport = new-Object -typeName psobject -property @{"NextDeltaLink" = $nextDeltaLink}
# Save the next delta link to a file inside the function app
Convertto-json $dataToExport | out-file $filePath -Encoding UTF8
}
# Send the result to the output (Here the response from the Http Request) (Optional)
Out-File -Encoding Utf8 -FilePath $res -inputObject $result
# send the result to the queue passed in parameter
if($result){
$resultObj = convertfrom-json $result
if($resultObj){
if($resultObj."members@delta"){
$StorageCtx = New-AzureStorageContext -ConnectionString $env:AzureWebJobsStorage
$outQueue = Get-AzureStorageQueue –Name $queueName -Context $StorageCtx
$msg = Convertto-Json ($resultObj."members@delta")
Write-Output "Add $msg to Queue $queueName"
$queueMessage = New-Object -TypeName Microsoft.WindowsAzure.Storage.Queue.CloudQueueMessage -ArgumentList $msg
$outQueue.CloudQueue.AddMessage($queueMessage)
}
}
}
}
else{
$message = "Please provide a value for the arguments 'groupid'.
Write-Output $message
Out-File -Encoding Utf8 -FilePath $res -inputObject $message
}
Below is a function helper to send an Http Request to Microsoft Graph API and process the results
function Send-GraphApiRequest{
param(
[Parameter(Mandatory = $true, HelpMessage = "uri")]
[String]
$uri,
[Parameter(Mandatory = $true, HelpMessage = "accessToken")]
[String]
$accessToken,
[Parameter(Mandatory = $true, HelpMessage = "method")]
[ValidateSet("Get", "Post")]
[String]
$method,
[Parameter(Mandatory = $false, HelpMessage = "body")]
[String]
$body
)
$results = @()
$deltaLink = $null
do
{
switch($method){
"Get"{
$graphResponse = Invoke-RestMethod -Method $method -Uri $uri -Headers $headers
}
"Post"{
$graphResponse = Invoke-RestMethod -Method $method -Uri $uri -Headers $headers -Body $body
}
default{
}
}
if ($graphResponse -and $graphResponse.Value -and $graphResponse.Value.Count) {
$results += $graphResponse.Value
$uri = $graphResponse."@odata.nextLink"
}
else {
$uri = $null
}
if($graphResponse."@odata.deltaLink"){
$deltaLink = $graphResponse."@odata.deltaLink"
}
}
while ($uri)
$resultJson = Convertto-json -inputObject $results -Depth 20
return New-Object -typename psobject -property @{"Results" = $resultJson; "NextDeltaLink" = $deltaLink}
}
The last function will not be explained here because it’s not part of the “generic” process and is only used to process the results according to your requirements (Assign licences, Change Policies, Update DataBase, Update User Profiles…)