I have a fun one to talk about today. This is the first part in a three-part series which covers the use of PowerShell and an Azure Function App to automatically change over Windows 11 devices to a new group tag post upgrading from Windows 10. This is something I have had cooking for a few weeks now at a test level and am now confident enough in to go ahead and bring to the masses.
Update:
Following my recent post on Security Updates for HTTP(s) Function Apps, I have started off by updating the Windows 11 Group Tag Conversion (GTC) Function App and client-side script. This means that the security concerns discussed in the prior versions of these articles no longer apply! However, as this is a major change, I decided it best to redact the previous articles and put out updated ones so folks would better see said updates. This header will be at the top of each of the new articles. The overall process is largely the same with only the authentication concerning portions and items like setup links changing.
Watch me talk about it instead!
Steve Weiner invited me on his GetRubix PODCAST to discuss this same solution. Check it out at the link below!
The GetRubix PODCAST – Episode 21: Analytics and Automation – YouTube
What problem does this solution solve?
As mentioned above, some organizations are building new Autopilot Group Tags for their Windows 11 Autopilot in order to tie new and freshly redone policy’s, provisioning scripts, apps, etc, to that new tag. This allows organizations to go back and fix/rebuild/update everything they might not have liked about their Windows 10 Autopilot solution and rebuild it in a better format for Windows 11 without impacting said existing Windows 10 solution. It also allows you to better develop a solution more focused to Windows 11 as it does have its own specific policy and setup needs. As a result of this, a new, clean, and seperate Windows 11A Autopilot process and tag is born.
That said, most organizations are not likely to hand every single employee a brand-new Windows 11 device and thus run every Windows 11 device through that new Autopilot. Most organizations want to be able to upgrade existing Windows 10 devices to Windows 11. In my opinion, this is not an unreasonable ask. However, this introduces two problems for anyone who did split Windows 10 and Windows 11 tags.
The first problem is that those devices you upgrade are still under their original Windows 10 tag and thus inheriting the Windows 10 policy tied to that tag and not your new shiny Windows 11 policy. The simple(ish) solution to that problem is to make a dynamic group that targets the Windows 11 OS, target that dynamic group as included to your Windows 11 policies/apps/etc, and exclude that group from your Windows 10 items. Thus, an upgraded device will then move over to Windows 11’s policies/apps/etc.
However, while that’s a simple answer, the devil is in the details. From what I have seen, once upgraded to Windows 11, a device will also Intune Wipe back to Windows 11. That means a Window 10 device which gets upgraded, then wiped, will then try to pass through Windows 10’s ESP as a Windows 11 device because it is still marked for Windows 10’s ESP profile via its old Group Tag. Sending Windows 11 devices through your old Windows 10 ESP is probably not going to go well. Unfortunately, you’re not going to be able to use the dynamic group you made to get around this one as there are a multitude of roadblocks there.
You can’t exclude from ESP profiles. While you can filter and could use a filter to kick out Windows 11 devices from your Windows 10 ESP, that only works if you didn’t use the default policy for your Windows 10 ESP. Even if you can do that, you need a way to include those devices to the Windows 11 ESP. The only way a dynamic group can evaluate on a non-enrolled device (which is the case in this situation) is if all the rules look to values that are part of the Azure AD record (further testing needed on that last point). While the OS version is part of the Azure AD record, if you try to do anything like model, manufacturer, management type, etc, the rule won’t be able to evaluate since that data is only part of the now non-existent Intune record. So, unless your Windows 10 is on its own ESP profile, and you are okay with excluding ALL windows 11 devices, and you are okay with including ALL Windows 11 devices to another profile, that won’t work. Even then I find that insanely messy. What would work best is just changing the group tag to the appropriate one.
So – That all begs the question, how can one automatically flip the group tag from A to B once a device upgrades to Windows 11?
The Solution:

Credit: If you are familiar with my work on Log Analytics, this will look very familiar. That’s because it’s once again based on the original Function App logic of the MSEndpointMGR team’s Log Analytics Function App. Specifically, Jan Ketil Skanke Function App from the Intune Enhanced Inventory project. That thing is just all sorts of handy! Additionally, now that its been updated in this 2.0 form, it makes extensive use of the AADDeviceTurst project by Nickolaj Andersen also of the MSEndpointMgr team. That project is the HTTPs function app Auth project I have talked about in other blogs such as the one linked under the highlighted “update” at the very top. I should clarify that not much remains of the original Intune Enhanced Inventory project Function App due to that update but it never hurts to give extra credit.
To anyone not familiar with this, it might look like a lot, but I promise it’s really not. Here is what happens.
- We make a Windows 11 Dynamic Group in Intune that picks up all relevant Windows 11 devices. That is, anything which can be autopilot enrolled. You might need to get create to avoid things like Virtual Devices.
- We will then use Intune Proactive Remediations which does have licensing requirements and prerequisites to send a PowerShell script to the devices in that group. This will happen on a repeating bases you can control. The script itself also contains some logic to avoid virtual devices and could be customized as much as you want to avoid anything else. It’s PowerShell, go nuts.
- That script will compile the devices Name, Serial Number, Desired Group Tag, Public Azure AD registration key, and a signature created from the private key. More details on the purpose of those last two items can be found here. This does assume you want all Windows 11 devices, or more so whatever devices you scope this script to in that dynamic group, under one tag. Again though, PowerShell logic could be added to change this.
- That information is turned into a JSON and passed up to our Function App using Function App level authorization. Please note that the key used to call the Function App will be in plain text inside this script which could be captured through a multiple number of means. However, as this Function App is hung out on the public internet, having a lock on the Function App with keys on all your clients is better than a box with no lock at all.
- When the Function App gets the request, it will then evaluate the information provided to validate the request came from a legitimate device. Again, if you want more information on how this is done, see this article: Security Update for HTTP(s) Function Apps – Getting the Most Out of Azure (azuretothemax.net)
- The Function will then check the existing tag of the device, see if it matches the tag the device wants to change to, and if not change it.
- The Function will then reply to the client letting it know what it did or did not do and why. For instance, it changed the tag of X device to Y, or did not because it was already under that tag.
- The script will log all its actions out to a local log file and make a note of what it did or did not do along with the response from the Function App if it called it. The script will also mark the time and date it last ran at all. If the script got a response from the Function, it will notate what tag the Function App last told it that it had.
- Lastly, when Intune eventually has the script re-run on the device, the script will first see if the tag you want to change to matches the last tag it got in response from the Function. If it does, it will do some math to evaluate when it last ran and will simply not contact the function if that time was within a certain controllable range. In other words, if it ran an hour ago and still wants to be under the tag it knows it last had, it’s not going to reach out to the Function App again. If it last ran 30 days ago, or now has a different tag to change to than it last knew, it will reach out and again confirm or change the tag as needed.
More exact details on how all of this works will be found in the next parts for the Function App and Client Script respectively.
Cost, Security, and all that other fun stuff:
As I mentioned, this Function App is very similar to that used in my Log Analytics Series. Instead of passing log files and the Function app uploading them to Log Analytics or a DCR, we are passing it machine info and a tag, and the Function App calls graph and makes changes as needed.
The concepts of how much it costs to run a Function App remain the same. For that reason, I am not going to recover those topics in any more detail than I just did minus a bit more info below.
If you want to know more about the cost of the Function app and application insights, see the sections A Function Example and Application Insights here: Collecting Custom PowerShell Log Analytics Data With DCR’s: Part 2 – Cost – Getting the Most Out of Azure (azuretothemax.net)
If you want to know more about the now improved security, you should refer to my new article here: Security Update for HTTP(s) Function Apps – Getting the Most Out of Azure (azuretothemax.net) – anything you read in other articles about the Azure and Tenant ID based authentication does NOT apply to this new Function App.
Note, the Function App for this series has the additional permission of making changes via Graph using the DeviceManagementServiceConfig.ReadWrite.All permission. While the function is only coded very specifically to make Group Tag changes, you can read about what all one could do with that permission here. Again though, the API key and new authentication methods stand in the way of anyone even making it to that point in the Function.
The Next Steps:
Disclaimer
The following is the disclaimer that applies to all scripts, functions, one-liners, setup examples, documentation, etc. This disclaimer supersedes any disclaimer included in any script, function, one-liner, article, post, etc.
You running this script/function or following the setup example(s) means you will not blame the author(s) if this breaks your stuff. This script/function/setup-example is provided AS IS without warranty of any kind. Author(s) disclaim all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall author(s) be held liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the script or documentation. Neither this script/function/example/documentation, nor any part of it other than those parts that are explicitly copied from others, may be republished without author(s) express written permission. Author(s) retain the right to alter this disclaimer at any time.
It is entirely up to you and/or your business to understand and evaluate the full direct and indirect consequences of using one of these examples or following this documentation.
The latest version of this disclaimer can be found at: https://azuretothemax.net/disclaimer/
