Balance Device Wave Groups for granular Intune deployments

As the CrowdStrike outage winds down and things return to normal for most users, now is a great time to think about update waves! If you’re using Autopatch, Microsoft has you covered for the most part. But if you’re not, this could be an interesting read for you!

We’ve developed a C# Function App designed to balance devices from a main group into various Wave groups. This app can run on a recurring schedule, rebalancing devices as needed when the number of devices changes due to growth or shrinkage.

Deploying your Windows update policies, Defender for Endpoint, or other configurations to these groups can help minimize the impact of any issues that arise with patches or other deployments.

Currently, only managed Windows and Mac Intune devices are balanced across the Wave groups.

We also offer a more advanced version of this Function App, which includes:

  • Key Vault Integration for App ID and Secrets: Securely manage your application credentials.
  • Multi-Tenant Configuration with Central Config: Manage multiple tenants with a centralized configuration.
  • Exclude Stale Devices from Waves: Automatically exclude devices that are no longer active.

Contact us to learn more about these advanced features and how they can help secure your organization.


An app registration with group read/write is required, replace the CONFIG values with the details from your tenant.

using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Graph.Models;

namespace FunctionAppExample
{
    public static class ManageDeviceGroups
    {
        [Function("TimerTrigger_Weekday_7AM")]
        public static void Run([TimerTrigger("0 0 7 * * 1-5")] TimerInfo myTimer, ILogger log)
        {
            log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
            //use this for time trigger, use http for testing
        }

            [Function("ManageDeviceGroups")]
        public static async Task<string> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
            FunctionContext context)
        {
            var log = context.GetLogger("ManageDeviceGroups");
            log.LogInformation("C# HTTP trigger function processed a request.");

            var waves = new Dictionary<int, wave>();

            //START CONFIG
            var tenantid = "";
            var appid = "";
            var secret = "";

            var maindevicegroup = "Baseline - Modern Workplace Devices";

            var wave1 = new wave();
            wave1.Id = 1;
            wave1.Name = "Baseline - Devices Wave 1";
            wave1.percentagegoal = 0.20f;
            waves.Add(1, wave1);

            var wave2 = new wave();
            wave2.Id = 2;
            wave2.Name = "Baseline - Devices Wave 2";
            wave2.percentagegoal = 0.30f;
            waves.Add(2, wave2);

            var wave3 = new wave();
            wave3.Id = 3;
            wave3.Name = "Baseline - Devices Wave 3";
            wave3.percentagegoal = 0.50f;
            waves.Add(3, wave3);
            //END CONFIG

            // Create a ClientSecretCredential instance
            var credential = new ClientSecretCredential(tenantid, appid, secret);

            // Create a GraphServiceClient instance with the credential
            var graphClient = new GraphServiceClient(credential);
            

            var moderndevicegroup = await graphClient.Groups.GetAsync((requestConfiguration) =>
            {
                requestConfiguration.QueryParameters.Count = true;
                requestConfiguration.QueryParameters.Filter = $"displayname eq '{maindevicegroup}'";
                requestConfiguration.QueryParameters.Select = new string[] { "id", "displayName" };
                requestConfiguration.Headers.Add("ConsistencyLevel", "eventual");
            });

            // Retrieve the devices from the source group
            var devices = await graphClient.Groups[moderndevicegroup.Value[0].Id].Members.GetAsync();

            var manageddevices = new List<Microsoft.Graph.Models.Device>();

            foreach (var device in devices.Value) {
                var managedDevice = device as Microsoft.Graph.Models.Device;
                if (!string.IsNullOrEmpty(managedDevice.ManagementType)) {
                    if (managedDevice != null && managedDevice.ManagementType.Contains("MDM"))
                    {
                        string os = managedDevice.OperatingSystem;
                        if (os.ToLower().Contains("windows") || os.ToLower().Contains("mac"))
                        {
                            manageddevices.Add(managedDevice);
                        }
                    }
                }
            }

            foreach (var wave in waves)
            {
                var wavegroup = await graphClient.Groups.GetAsync((requestConfiguration) =>
                {
                    requestConfiguration.QueryParameters.Count = true;
                    requestConfiguration.QueryParameters.Filter = $"displayName eq '{wave.Value.Name}'";
                    requestConfiguration.QueryParameters.Select = new string[] { "id", "displayName"};
                    requestConfiguration.Headers.Add("ConsistencyLevel", "eventual");
                });

                //Update the wave
                wave.Value.entraid = wavegroup.Value[0].Id;
                await wave.Value.updatewaveAsync(graphClient, manageddevices.Count());
            }

            foreach (var wave in waves)
            {
                if (wave.Value.change == false)
                {
                    continue;
                }

                var difference = wave.Value.countgoal - wave.Value.count;
                if (difference < 0)
                {
                    //need to remove from this wave
                    foreach (var device in wave.Value.devices)
                    {
                        if (difference == 0)
                        {
                            break;
                        }
                        try
                        {
                            await graphClient.Groups[wave.Value.entraid].Members[device.Id].Ref.DeleteAsync();
                        }
                        catch (Exception e)
                        {
                            log.LogError(e.ToString());
                        }
                        difference++;
                    }
                    await wave.Value.updatewaveAsync(graphClient, manageddevices.Count());
                }
            }

            foreach (var wave in waves)
            {
                if (wave.Value.change == false)
                {
                    continue;
                }

                var difference = wave.Value.countgoal - wave.Value.count;
                if (difference > 0)
                {
                    //need to add to this wave
                    var availabledevices = GetUnassignedDevices(manageddevices, waves, difference);
                    foreach (var device in availabledevices)
                    {
                        try
                        {

                            var requestBody = new ReferenceCreate
                            {
                                OdataId = $"https://graph.microsoft.com/v1.0/directoryObjects/{device.Id}"
                            };

                            await graphClient.Groups[wave.Value.entraid].Members.Ref.PostAsync(requestBody);

                            difference--;
                        }
                        catch (Exception e)
                        {
                            log.LogError(e.ToString());
                        }
                    }
                    await wave.Value.updatewaveAsync(graphClient, manageddevices.Count());
                }
            }
        return "ok";
        }

        public static List<Microsoft.Graph.Models.Device> GetUnassignedDevices(List<Microsoft.Graph.Models.Device> manageddevices, Dictionary<int, wave> waves, int count)
        {
            var unassigneddevices = new List<Microsoft.Graph.Models.Device>();
            int cycle = 0;
            foreach (var device in manageddevices)
            {
                var duplicate = false;
                foreach (var substwave in waves)
                {
                    foreach (var substdevice in substwave.Value.devices)
                    {
                        if (substdevice.Id == device.Id)
                        {
                            duplicate = true;
                        }
                    }
                }
                if (duplicate) { continue; }
                unassigneddevices.Add(device);
                cycle++;
                if (cycle == count) { break; }
            }
            return unassigneddevices;
        }
    }

    public class wave
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public float percentagegoal { get; set; }
        public int count { get; set; }
        public int countgoal { get; set; }
        public bool change { get; set; }
        public List<DirectoryObject?> devices { get; set; }
        public string entraid { get; set; }

        public async Task updatewaveAsync(GraphServiceClient graphClient, int totaldevices)
        {
            var wavedevices = await graphClient.Groups[entraid].Members.GetAsync();
            this.count = wavedevices.Value.Count();
            this.devices = wavedevices.Value;

            this.countgoal = (int)(totaldevices * this.percentagegoal);

            if (this.count != this.countgoal < 0)
            {
                this.change = true;
            }
            return;
        }
    }
}
Code language: JavaScript (javascript)

No responses yet

Leave a Reply

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