Organize AutoPilot devices in dynamic AAD groups using GroupTags & PowerShell

Don’t ever say Microsoft doesn’t listen! One of my biggest pet peeves was solved at the beginning of the month when Microsoft announced the ability to edit device group tags!

This doesn’t sound like much, but it essentially unlocks the potential of group tags that was never really there before - we can now use group tags to dynamically control device group membership.

While there have been quite a few great posts about this, most of the code in the articles I’ve read are using community modules that, while there’s nothing wrong with using other people’s code, I find the best way to understand how something works is to dig in and figure out the inner-workings myself.

The good news is that because almost *everything* in Azure is accessible through the omnipresent Microsoft Graph, so it only took a few minutes of analyzing the requests to figure out what was going on. Let’s dive right in!

First let’s set up a few dynamic groups in AAD.

The first one - let’s call this our “baseline” group, will have the following dynamic rule that will capture all devices.

(device.devicePhysicalIDs -any _ -contains "[ZTDId]")

The second group - let’s call this our “exclusion” group, will have the following dynamic rule that will capture all devices with a specific group tag.

(device.devicePhysicalIds -any _ -eq "[OrderID]:EXCLUDE")

Once set up and given enough time to update, you should now see all of your devices in the “baseline” group and nothing in the “exclusion” group. Let’s fix that now.

Go and grab the serial numbers of the devices you wish to move to the exclusion group (you can grab this directly from AAD or from the Windows AutoPilot Devices section of Intune - here’s mine as an example - note that there are no group tags assigned)

Autopilot devices

For this example, let’s exclude the first device in the screenshot above.

The first step when we interact with Microsoft Graph, as always, is to authenticate and store the Authentication Token in a variable so that we can use it for the next few steps - I’ve spoken about this before, so if you aren’t sure how to do this, read this and come back when you are ready!

$authToken = Get-AuthToken #use one of a million ways to get the authtoken...

Alright - we’ve got our auth token, put your serial number into a variable, and then we will form out the first Graph Call.

$serialNumber = "Put Your Serial Number Here"
$graphUri = "`$filter=contains(serialNumber,'$serialNumber')"
$result = (Invoke-RestMethod -Method Get -Uri $graphUri -headers $authToken).value

If we’ve formed our request properly, if we look at the contents of $result, we should see details on the device in question.

$result data

Next, we are going to grab the id of the device from the graph call, build the group tag data and post it back to Graph..

$id = $
$body = @{
    groupTag = "EXCLUDE"
Invoke-RestMethod -Method POST -Uri "$id/updateDeviceProperties" -Body ($body | ConvertTo-Json -Compress) -Headers $script:authToken

Finally, we are going to perform a sync on our AutoPilot device list..

Invoke-RestMethod -Method Post -Uri "" -Headers $script:authToken

Now if we jump over to our AutoPilot devices list we should see the group tag updated..

Updated group tag

And of course, our dynamic groups should now be populated correctly..

Dynamic groups

That’s it! - 3 calls to Graph and a little bit of patience.

Now of course, 3 REST API calls is not how I’m going to leave you - with a little bit of error handling and polish we can build out a very reliable function..

function Update-AutoPilotGroupTag {
    param (
        [parameter(Mandatory = $true)]

        [parameter(Mandatory = $false)]

        [parameter(Mandatory = $false)]
    try {
        if (!($script:authToken)) {
            $script:authToken = Get-AuthToken -user $upn
        $baseUri = ''
        $apDevices = foreach ($sn in $deviceSerial) {
            #make sure the device identity exists
            $deviceId = (Invoke-RestMethod -Method Get -Uri "$baseUri`?`$filter=contains(serialNumber,'$sn')" -Headers $script:authToken).value
            if ($deviceId) {
                Write-Host "Found device with id: $deviceSerial"
                $body = @{
                    groupTag = $(if ($groupTag) { $groupTag } else { '' })
                $update = Invoke-WebRequest -Method Post -Uri "$baseUri/$($" -Body ($body | ConvertTo-Json -Compress) -Headers $script:authToken -UseBasicParsing
                if ($update.StatusCode -eq 200) {
                   Write-Host "Updated device: $deviceSerial with grouptag: $groupTag"
                else {
                    throw "Web requested failed with status code: $update.statusCode"
    catch {
        $errorMsg = $_.Exception.Message
    finally {
        if ($errorMsg) {
            Write-Warning $errorMsg
        else {
            if ($sync) {
                Write-Host "Autopilot device sync requested.."
                Invoke-RestMethod -Method Post -Uri "" -Headers $script:authToken

As always, all code from this post will be available on my GitHub and I am always available to chat on Twitter.

– Ben