Setting a Group Tag During a Configuration Manager Task Sequence
In this post I’m going to talk about using the Windows autopilot deployment for existing devices Task sequence
in configuration manager and modifying that task sequence to set a group tag
during the process. You may know that traditionally this tasks sequence is used to ‘build’ devices and have them ready to start the autopilot process. I had a customer recently that had that very need. They had purchased a large number of machines that were delivered with Windows 10 installed and not Windows 11. As the customer was on their Windows 11 uplift journey already they were a bit disappointed to say the least and they opted to create a Configuration Manager task sequence to prep them for autopilot. That prep process was to include wiping the device and re-loading with Windows 11, adding the device to the tenants autopilot devices AND setting a group tag to drop the device into pre-created groups that control the apps and configurations the devices are going to receive during autopilot.
So… lets get started…
App Registration
The first thing you are going to need to create is an application registration . This app registration allows you to add devices to autopilot and set a group tag. We will work on the basis of least privilege and only allow the app registration a singular permission. The application registration will be called upon during out task sequence where we will pass our secret key to allow us to use it.
- Within the Microsoft Entra ID portal, navigate to App Registrations.
- Create a new Registration and name it as you see fit, in my case I called it “Autopilot GroupTag” you know, because I’m super inventive.
- You will now need to add the permission DeviceManagementServiceConfig.ReadWrite.All, as a Microsoft Graph Application Permission and grant consent.
- Finally you will need to create an application Secret and copy the secret key (Take note of the expiry date and set reminders for yourself and also, ensure to store the key safely)
Before continuing on you will need the following items:
- Your Directory (tenant) ID (Overview Blade of your app registration)
- The application (Client) ID (Overview Blade of your app registration)
- Your secret value (Certificates and Secrets Blade of your app registration)
Task Sequence
OK, lets now configure the task sequence. First create your autopilot task sequence following the Microsoft documentation from here .
I wont regurgitate the task sequence configuration steps from the above documentation, however once you have the task sequence set up we’re going to make some adjustments. We are firstly going to create four variables
TenantID AppID SecretID GroupTag |
![]() |
For the SecretID, I strongly suggest you tick the box to “Do Not Display this Value”. This at least forms some sort of security for you as the secret is the key to the app registration and what it can do. Keeping that secure may or may not be important to you:
A description containing the expiry date might help in the event of failures too :-)
The Script
Next comes the fun bit. I lost a considerable amount of time testing this with numerous faults and errors. I was hoping that, I could use the Get-WindowsAuotpiloInfo script during the sequence to ‘just get it done’. But I ran into multiple issues. Namely, installing modules during WinPE because no matter what I tried, I was unable to leverage “Install-Module” correctly. The original script has some pre-req modules and so wouldn’t run without them present. This was causing me a headache. I then found the Recast Software community post (see references) where they used Invoke-Webrequest to pull in the module and then essentially move it to the correct location. This worked a treat.
Here is the script I used. Explanation below.
<#
.SYNOPSIS
Installs Get-WindowsAuopilotInfo.ps1 and calls it using the parameter
.DESCRIPTION
* Used alongside a task sequence within Configuration Manager, this script was uses to add the device to autopilot and set a GroupTa
.AUTHOR
Jonathan Fallis - www.deploymentshare.com
.VERSION
1.0.1 - Added Error Logging
1.0.0 - Original
.EXAMPLE
.\Set-AutopilotGroupTag -TenantID "123456" -AppID "234567" -SecretID "345678" -GroupTag "AutopilotDevice"
#>
Param(
[Parameter(Mandatory=$true)]
[string]$TenantID,
[Parameter(Mandatory=$true)]
[string]$AppID,
[Parameter(Mandatory=$true)]
[string]$SecretID,
[Parameter(Mandatory=$true)]
[string]$GroupTag
)
$WorkingDir = $env:TEMP
$LogFilePath = "C:\Windows\Temp\Set-GroupTag.log"
[System.Environment]::SetEnvironmentVariable('LOCALAPPDATA',"$env:SystemDrive\Windows\system32\config\systemprofile\AppData\Local")
#Function for Error Logging
Function Write-log {
[CmdletBinding()]
Param(
[parameter(Mandatory = $true)]
[String]$Path,
[parameter(Mandatory = $true)]
[String]$Message,
[parameter(Mandatory = $true)]
[String]$Component,
[Parameter(Mandatory = $true)]
[ValidateSet('Info', 'Warning', 'Error')]
[String]$Type
)
switch ($Type) {
'Info' { [int]$Type = 1 }
'Warning' { [int]$Type = 2 }
'Error' { [int]$Type = 3 }
}
# Create a log entry
$Content = "<![LOG[$Message]LOG]!>" + `
"<time=`"$(Get-Date -Format 'HH:mm:ss.ffffff')`" " + `
"date=`"$(Get-Date -Format 'M-d-yyyy')`" " + `
"component=`"$Component`" " + `
"context=`"$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " + `
"type=`"$Type`" " + `
"thread=`"$([Threading.Thread]::CurrentThread.ManagedThreadId)`" " + `
"file=`"`">"
# Write the line to the log file
Add-Content -Path $Path -Value $Content
}
#Test for internet connectivty using 8.8.8.8
If (Test-Connection 8.8.8.8 -quiet) {
Write-Log -Type Info -Message "Internet Connection OK" -Component "Internet Check" -Path $LogFilePath
}
Else {
Write-Log -Type Error -Message "Internet Connection check failed" -Component "Internet Check" -Path $LogFilePath ; Exit 1
}
#Enable TLS 1.2
Try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Log -Type Info -Message "Setting TLS1.2 Completed Successfully" -Component "TLS 1.2 Check" -Path $LogFilePath
}
Catch {
Write-Log -Type Error -Message ($_ | Out-String) -Component "TLS 1.2 Check" -Path $LogFilePath
}
#PowerShellGet from PSGallery URL
Try {
if (!(Get-Module -Name PowerShellGet)){
$PowerShellGetURL = "https://psg-prod-eastus.azureedge.net/packages/powershellget.2.2.5.nupkg"
Write-Log -Type Info -Message "URL set to $PowerShellGetURL" -Component "PowerShellGet Check" -Path $LogFilePath
Invoke-WebRequest -UseBasicParsing -Uri $PowerShellGetURL -OutFile "$WorkingDir\powershellget.2.2.5.zip"
Write-Log -Type Info -Message "Downloaded PowerShellGet " -Component "PowerShellGet Check" -Path $LogFilePath
$Null = New-Item -Path "$WorkingDir\2.2.5" -ItemType Directory -Force
Expand-Archive -Path "$WorkingDir\powershellget.2.2.5.zip" -DestinationPath "$WorkingDir\2.2.5"
Write-Log -Type Info -Message "Unzipped PowerShellGet " -Component "PowerShellGet Check" -Path $LogFilePath
$Null = New-Item -Path "$env:ProgramFiles\WindowsPowerShell\Modules\PowerShellGet" -ItemType Directory -ErrorAction SilentlyContinue
Move-Item -Path "$WorkingDir\2.2.5" -Destination "$env:ProgramFiles\WindowsPowerShell\Modules\PowerShellGet\2.2.5"
Write-Log -Type Info -Message "Moved PowerShellGet to $WorkingDir" -Component "PowerShellGet Check" -Path $LogFilePath
}
}
Catch {
Write-Log -Type Error -Message ($_ | Out-String) -Component "PowerShellGet Check" -Path $LogFilePath
}
#PackageManagement from PSGallery URL
Try {
if (!(Get-Module -Name PackageManagement)){
$PackageManagementURL = "https://psg-prod-eastus.azureedge.net/packages/packagemanagement.1.4.7.nupkg"
Write-Log -Type Info -Message "URL set to $PackageManagementURL" -Component "PackageManagement Check" -Path $LogFilePath
Invoke-WebRequest -UseBasicParsing -Uri $PackageManagementURL -OutFile "$WorkingDir\packagemanagement.1.4.7.zip"
Write-Log -Type Info -Message "Downloaded PackageManagement" -Component "PackageManagement Check" -Path $LogFilePath
$Null = New-Item -Path "$WorkingDir\1.4.7" -ItemType Directory -Force
Expand-Archive -Path "$WorkingDir\packagemanagement.1.4.7.zip" -DestinationPath "$WorkingDir\1.4.7"
Write-Log -Type Info -Message "Unzipped PackageManagement" -Component "PackageManagement Check" -Path $LogFilePath
$Null = New-Item -Path "$env:ProgramFiles\WindowsPowerShell\Modules\PackageManagement" -ItemType Directory -ErrorAction SilentlyContinue
Move-Item -Path "$WorkingDir\1.4.7" -Destination "$env:ProgramFiles\WindowsPowerShell\Modules\PackageManagement\1.4.7"
Write-Log -Type Info -Message "Moved PackageManagement to $WorkingDir" -Component "PackageManagement Check" -Path $LogFilePath
}
}
Catch {
Write-Log -Type Error -Message ($_ | Out-String) -Component "PackageManagement Check" -Path $LogFilePath
}
#Import PowerShellGet
if (Import-Module PowerShellGet) {
Write-Log -Type Info -Message "PowerShellGet Module Imported OK" -Component "PowerShellGet Import" -Path $LogFilePath
}
Else {
Write-Log -Type Error -Message ($_ | Out-String) -Component "PowerShellGet Import" -Path $LogFilePath ; Exit 1
}
#Install the script
if (Install-Script Get-WindowsAutopilotinfo -Force) {
Write-Log -Type Info -Message "Get-WindowsAutopilotInfo Installed OK" -Component "Get-WindowsAutopilotInfo Install" -Path $LogFilePath
}
Else {
Write-Log -Type Error -Message ($_ | Out-String) -Component "Get-WindowsAutopilotInfo Install" -Path $LogFilePath ; Exit 1
}
#Run the script
if (Get-WindowsAutopilotinfo -Online -TenantId $TenantID -AppId $AppID -AppSecret $SecretID -Grouptag $GroupTag) {
Write-Log -Type Info -Message "Get-WindowsAutopilotInfo executed successfully" -Component "Running Get-WindowsAutopilotInfo" -Path $LogFilePath
}
Else {
Write-Log -Type Error -Message ($_ | Out-String) -Component "Get-WindowsAutopilotInfo Install" -Path $LogFilePath ; Exit 1
}
- Changes 08/01/2024 - Added Error Logging to the script to output to a log file in C:\Windows\Temp - aids troubleshooting.
Script Step
Lets create a PowerShell Script step in our Task sequence.
We will opt to past a script into the step;
We will also configure the script to pass in four parameters.
Notice that the script requires four parameters passing to it and would you believe it!? they match the task sequence variables we set up earlier. Wonderful. to reference a task sequence variable in a task sequence step you surround the text with “%” like in the above image.
Within the step you will notice the following;
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
This enables TLS 1.2. This is used to overcome networking issues experienced when attempting to go out to the www to receive content. There are then two sections which manually pull in the modules PowerShellGet and PackageManagement, both required to install the “Get-WindowsAutopilotInfo” script (which ensures the latest version is always pulled in) and to run it using the parameters we have passed in because… the script Get-WindowsAutopilotInfo uses the same parameter names we have passed in from our task sequence variables.
In testing I see the devices appear in Autopilot devices;
And then shortly after the device that is “Not Assigned” changes to pending, and lastly - assigned
And VOILA! Device present and accounted for.
Happy Customer.
Testing
Once testing begun there was an issue we noticed. Shift+F10 was not available in the event of a failure which was odd. This is controlled by the file C:\Windows\Setup\Scripts\DisableCMDRequest.TAG which we can remove with a simple Run Command Line Step.
Improvements / Ideas
- If you are not too scared about human-error, you could use something like ServiceUI to prompt the end user for text input to set the group tag and configure the variable GroupTag to the input of that.
- You could have a dynamic group tag set on hardware type using WMI queries. If you know a certain model requires a certain group tag you can set this condition on the Options tab of the task sequence variable step.
Thanks for reading
Jonathan.
Location | Link |
---|---|
Powershell is Fun | Upload Autopilot Hardware Hash |
Recast Software | Enable PSGallery in a Task Sequence |