Introduction
When a client checks for Group Policy updates, it follows a structured process to determine what needs to be applied.
Version : Before doing anything, the client compares its locally cached GPO version against the remote version, stored both as a versionNumber attribute on the GPO object and in the GPT.ini file on SYSVOL. If the remote version isn’t newer, processing stops there (unless gpupdate /force is used).
CSE GUIDs extensions : If an update is detected, the client inspects the gPCMachineExtensionNames and gPCUserExtensionNames attributes to know which Client-Side Extensions (CSEs) to load. For Immediate Scheduled Tasks specifically, two GUID blocks must be present (one referencing the Core GPO Engine and Scheduled Tasks CSEs and another pairing the Preference CSE with the Scheduled Tasks tool CSE. Without these GUIDs, the client won’t even look at scheduled task configuration.
ScheduledTasks.xml : Once the correct CSEs are identified, the client fetches ScheduledTasks.xml from a predictable SYSVOL path, parses it, and registers the tasks locally.
In short, three things must align for a GPO-based scheduled task to apply correctly: the version number must be incremented, the right CSE GUIDs must be declared on the GPO object, and the ScheduledTasks.xml file must exist in the expected SYSVOL location.
Script
# ============================================================
# GPO Scheduled Task Extension Configurator
# - Backs up GPO before any modification
# - Adds required CSE GUIDs for Immediate Scheduled Tasks
# - Restores Extensions from a previous backup
# ============================================================
Import-Module GroupPolicy
Import-Module ActiveDirectory
# ---------- CONFIGURATION ----------
$GPOName = "Ma GPO"
$AD = "<domain>"
$DomainDN = (Get-ADDomain -Server $AD).DistinguishedName
$DomainFQDN = (Get-ADDomain -Server $AD).DNSRoot
$BackupPath = Join-Path -Path (Get-Location) -ChildPath "GPO_Backups"
$PSDefaultParameterValues["*-AD*:Server"] = $AD
# -----------------------------------
# Required CSE GUIDs for Immediate Scheduled Tasks (Machine scope)
$RequiredExtensionString = "[{00000000-0000-0000-0000-000000000000}{79F92669-4224-476C-9C5C-6EFB4D87DF4A}{CAB54552-DEEA-4691-817E-ED4A4D1AFC72}][{AADCED64-746C-4633-A97C-D61349046527}{CAB54552-DEEA-4691-817E-ED4A4D1AFC72}]"
# GUID friendly names for display
$GUIDNames = @{
"{00000000-0000-0000-0000-000000000000}" = "Core GPO Engine"
"{79F92669-4224-476C-9C5C-6EFB4D87DF4A}" = "Preference Tool CSE - Local Users and Groups"
"{CAB54552-DEEA-4691-817E-ED4A4D1AFC72}" = "Preference Tool CSE - Scheduled Tasks"
"{AADCED64-746C-4633-A97C-D61349046527}" = "Preference CSE - Scheduled Tasks"
}
# ============================================================
# HELPER - Pretty print extension blocks
# ============================================================
function Format-ExtensionBlocks {
param ([string]$ExtensionString)
if ([string]::IsNullOrWhiteSpace($ExtensionString)) {
Write-Host " (empty)" -ForegroundColor DarkGray
return
}
$blocks = [regex]::Matches($ExtensionString, '\[\{[A-F0-9\-\}\{]+\}\]')
$blockIndex = 1
foreach ($block in $blocks) {
Write-Host " Block $blockIndex : $($block.Value)" -ForegroundColor White
$guids = [regex]::Matches($block.Value, '\{[A-F0-9\-]+\}')
foreach ($guid in $guids) {
$name = $GUIDNames[$guid.Value.ToUpper()]
if ($name) {
Write-Host " $($guid.Value) -> $name" -ForegroundColor DarkCyan
} else {
Write-Host " $($guid.Value)" -ForegroundColor DarkGray
}
}
$blockIndex++
}
}
# ============================================================
# HELPER - Merge extension blocks
# ============================================================
function Merge-ExtensionBlocks {
param (
[string]$Current,
[string]$Required
)
$CurrentBlocks = [regex]::Matches($Current, '\[\{[A-F0-9\-\}\{]+\}\]') | ForEach-Object { $_.Value.ToUpper() }
$RequiredBlocks = [regex]::Matches($Required, '\[\{[A-F0-9\-\}\{]+\}\]') | ForEach-Object { $_.Value.ToUpper() }
$MergedBlocks = [System.Collections.Generic.List[string]]($CurrentBlocks)
foreach ($block in $RequiredBlocks) {
if ($MergedBlocks -notcontains $block) {
$MergedBlocks.Add($block)
}
}
$Sorted = $MergedBlocks | Sort-Object
return $Sorted -join ""
}
# ============================================================
# HELPER - Read gPCMachineExtensionNames from a backup XML
# ============================================================
function Get-ExtensionsFromBackup {
param ([string]$BackupFolder)
$BkupInfo = Join-Path $BackupFolder "bkupInfo.xml"
if (-not (Test-Path $BkupInfo)) {
Write-Host "[-] bkupInfo.xml not found in: $BackupFolder" -ForegroundColor Red
return $null
}
[xml]$xml = Get-Content $BkupInfo -Encoding UTF8
$node = $xml.SelectSingleNode("//MachineExtensionGuids")
if ($null -eq $node) {
Write-Host "[!] No MachineExtensionGuids node found in backup XML." -ForegroundColor Yellow
return ""
}
return $node.InnerText.Trim()
}
# ============================================================
# MODE SELECTION
# ============================================================
Write-Host "`n============================================================" -ForegroundColor DarkYellow
Write-Host " GPO Scheduled Task Extension Configurator" -ForegroundColor DarkYellow
Write-Host "============================================================" -ForegroundColor DarkYellow
Write-Host "`n [1] Apply required CSE extensions to GPO"
Write-Host " [2] Restore Extensions from backup`n"
$ModeInput = Read-Host "Select mode (1 or 2)"
switch ($ModeInput) {
"1" { $Mode = "Apply" }
"2" { $Mode = "Restore" }
default {
Write-Host "`n[-] Invalid selection. Exiting." -ForegroundColor Red
exit 1
}
}
# ============================================================
# STEP 1 - Retrieve GPO
# ============================================================
Write-Host "`n[*] Retrieving GPO: '$GPOName'..." -ForegroundColor Cyan
try {
$GPO = Get-GPO -Name $GPOName -ErrorAction Stop
Write-Host "[+] GPO found: $($GPO.DisplayName) | ID: $($GPO.Id)" -ForegroundColor Green
} catch {
Write-Host "[-] GPO '$GPOName' not found. Exiting." -ForegroundColor Red
exit 1
}
$GPOPath = "CN={$($GPO.Id)},CN=Policies,CN=System,$DomainDN"
# ============================================================
# STEP 2 - Read current gPCMachineExtensionNames from AD
# ============================================================
Write-Host "`n[*] Reading current gPCMachineExtensionNames from AD..." -ForegroundColor Cyan
try {
$ADObject = Get-ADObject -Identity $GPOPath -Server $AD -Properties gPCMachineExtensionNames -ErrorAction Stop
$CurrentExtensions = $ADObject.gPCMachineExtensionNames
} catch {
Write-Host "[-] Failed to read AD object: $_" -ForegroundColor Red
exit 1
}
# ============================================================
# RESTORE MODE
# ============================================================
if ($Mode -eq "Restore") {
Write-Host "`n[*] Scanning backup folder: $BackupPath" -ForegroundColor Cyan
if (-not (Test-Path $BackupPath)) {
Write-Host "[-] Backup folder not found: $BackupPath" -ForegroundColor Red
exit 1
}
# List all backup sub-folders that belong to this GPO (matched by GUID inside bkupInfo.xml)
$BackupFolders = Get-ChildItem -Path $BackupPath -Directory |
Where-Object {
$infoFile = Join-Path $_.FullName "bkupInfo.xml"
if (Test-Path $infoFile) {
[xml]$x = Get-Content $infoFile -Encoding UTF8
$idNode = $x.SelectSingleNode("//GPOId")
$idNode -and ($idNode.InnerText.Trim(" {}") -eq $GPO.Id.ToString())
}
} | Sort-Object LastWriteTime -Descending
if ($BackupFolders.Count -eq 0) {
Write-Host "[-] No backups found for GPO '$GPOName' (ID: $($GPO.Id)) in $BackupPath" -ForegroundColor Red
exit 1
}
# Display available backups
Write-Host "`n Available backups for '$GPOName':`n" -ForegroundColor Yellow
$i = 1
$BackupList = @()
foreach ($folder in $BackupFolders) {
$ext = Get-ExtensionsFromBackup -BackupFolder $folder.FullName
Write-Host " [$i] $($folder.Name)" -ForegroundColor White
Write-Host " Date : $($folder.LastWriteTime)" -ForegroundColor DarkGray
Write-Host " Path : $($folder.FullName)" -ForegroundColor DarkGray
if ([string]::IsNullOrWhiteSpace($ext)) {
Write-Host " Extensions: (none recorded)" -ForegroundColor DarkGray
} else {
Write-Host " Extensions:" -ForegroundColor DarkGray
Format-ExtensionBlocks -ExtensionString $ext
}
Write-Host ""
$BackupList += [PSCustomObject]@{ Index = $i; Folder = $folder.FullName; Extensions = $ext }
$i++
}
$SelInput = Read-Host "Enter the number of the backup to restore from (or 'q' to quit)"
if ($SelInput -eq 'q') {
Write-Host "`n[!] Restore cancelled." -ForegroundColor Yellow
exit 0
}
$SelIndex = [int]$SelInput
$Selected = $BackupList | Where-Object { $_.Index -eq $SelIndex }
if ($null -eq $Selected) {
Write-Host "[-] Invalid selection." -ForegroundColor Red
exit 1
}
$RestoredExtensions = $Selected.Extensions
# Show change summary
Write-Host "`n============================================================" -ForegroundColor DarkYellow
Write-Host " RESTORE SUMMARY" -ForegroundColor DarkYellow
Write-Host "============================================================" -ForegroundColor DarkYellow
Write-Host "`n GPO Name : $($GPO.DisplayName)"
Write-Host " GPO ID : $($GPO.Id)"
Write-Host " Domain : $DomainFQDN"
Write-Host " Backup : $($Selected.Folder)"
Write-Host "`n--- CURRENT state (gPCMachineExtensionNames) ---" -ForegroundColor Yellow
Format-ExtensionBlocks -ExtensionString $CurrentExtensions
Write-Host "`n--- WILL BE RESTORED TO ---" -ForegroundColor Green
if ([string]::IsNullOrWhiteSpace($RestoredExtensions)) {
Write-Host " (empty - attribute will be cleared)" -ForegroundColor DarkGray
} else {
Format-ExtensionBlocks -ExtensionString $RestoredExtensions
}
Write-Host "`n============================================================" -ForegroundColor DarkYellow
# Safety backup before restore
Write-Host "`n[*] Creating a safety backup before restore..." -ForegroundColor Cyan
try {
$SafetyBackup = Backup-GPO -Name $GPOName -Path $BackupPath -ErrorAction Stop
Write-Host "[+] Safety backup created. ID: $($SafetyBackup.Id)" -ForegroundColor Green
} catch {
Write-Host "[-] Safety backup failed: $_" -ForegroundColor Red
$proceed = Read-Host "Continue without safety backup? (yes/no)"
if ($proceed -notin @("yes", "y")) {
exit 1
}
}
# Confirmation
$confirmation = Read-Host "`nApply restore to AD? (yes/no)"
if ($confirmation -notin @("yes", "y")) {
Write-Host "`n[!] Restore cancelled by user." -ForegroundColor Yellow
exit 0
}
# Apply restored value
Write-Host "`n[*] Writing restored extensions to AD..." -ForegroundColor Cyan
try {
if ([string]::IsNullOrWhiteSpace($RestoredExtensions)) {
Set-ADObject -Identity $GPOPath -Server $AD -Clear gPCMachineExtensionNames -ErrorAction Stop
} else {
Set-ADObject -Identity $GPOPath -Server $AD -Replace @{ gPCMachineExtensionNames = $RestoredExtensions } -ErrorAction Stop
}
Write-Host "[+] gPCMachineExtensionNames restored successfully." -ForegroundColor Green
} catch {
Write-Host "[-] Failed to restore: $_" -ForegroundColor Red
exit 1
}
# Verify
Write-Host "`n[*] Verifying restored value..." -ForegroundColor Cyan
$Verify = Get-ADObject -Identity $GPOPath -Server $AD -Properties gPCMachineExtensionNames
Write-Host "`n--- APPLIED state (gPCMachineExtensionNames) ---" -ForegroundColor Green
Format-ExtensionBlocks -ExtensionString $Verify.gPCMachineExtensionNames
Write-Host "`n[+] Restore complete for '$GPOName'." -ForegroundColor Green
Write-Host "[+] Safety backup available at: $BackupPath`n" -ForegroundColor Green
exit 0
}
# ============================================================
# APPLY MODE (original logic)
# ============================================================
# STEP 3 - Backup GPO
Write-Host "`n[*] Creating backup in: $BackupPath" -ForegroundColor Cyan
if (-not (Test-Path $BackupPath)) {
New-Item -ItemType Directory -Path $BackupPath | Out-Null
}
try {
$Backup = Backup-GPO -Name $GPOName -Path $BackupPath -ErrorAction Stop
Write-Host "[+] Backup successful." -ForegroundColor Green
Write-Host " Backup ID : $($Backup.Id)"
Write-Host " Backup Path: $BackupPath"
Write-Host " Timestamp : $($Backup.BackupTime)"
} catch {
Write-Host "[-] Backup failed: $_" -ForegroundColor Red
exit 1
}
# STEP 4 - Compute new extension value
$NewExtensions = Merge-ExtensionBlocks -Current $CurrentExtensions -Required $RequiredExtensionString
# STEP 5 - Display change summary and ask for confirmation
Write-Host "`n============================================================" -ForegroundColor DarkYellow
Write-Host " CHANGE SUMMARY" -ForegroundColor DarkYellow
Write-Host "============================================================" -ForegroundColor DarkYellow
Write-Host "`n GPO Name : $($GPO.DisplayName)"
Write-Host " GPO ID : $($GPO.Id)"
Write-Host " Domain : $DomainFQDN"
Write-Host "`n--- CURRENT state (gPCMachineExtensionNames) ---" -ForegroundColor Yellow
Format-ExtensionBlocks -ExtensionString $CurrentExtensions
Write-Host "`n--- NEW state after change (gPCMachineExtensionNames) ---" -ForegroundColor Green
Format-ExtensionBlocks -ExtensionString $NewExtensions
$CurrentBlocks = [regex]::Matches($CurrentExtensions.ToUpper(), '\[\{[A-F0-9\-\}\{]+\}\]') | ForEach-Object { $_.Value }
$NewBlocks = [regex]::Matches($NewExtensions.ToUpper(), '\[\{[A-F0-9\-\}\{]+\}\]') | ForEach-Object { $_.Value }
$AddedBlocks = $NewBlocks | Where-Object { $CurrentBlocks -notcontains $_ }
if ($AddedBlocks.Count -eq 0) {
Write-Host "`n[=] No changes needed - all required extensions already present." -ForegroundColor Green
exit 0
}
Write-Host "`n--- BLOCKS TO BE ADDED ---" -ForegroundColor Magenta
foreach ($block in $AddedBlocks) {
Write-Host " + $block" -ForegroundColor Magenta
}
Write-Host "`n============================================================" -ForegroundColor DarkYellow
Write-Host "[!] A backup has already been saved to: $BackupPath" -ForegroundColor Cyan
Write-Host "============================================================`n" -ForegroundColor DarkYellow
$confirmation = Read-Host "Apply these changes to AD? (yes/no)"
if ($confirmation -notin @("yes", "y")) {
Write-Host "`n[!] Operation cancelled by user. No changes were made." -ForegroundColor Yellow
Write-Host "[!] Backup is still available at: $BackupPath`n" -ForegroundColor Yellow
exit 0
}
# STEP 6 - Apply changes to AD
Write-Host "`n[*] Applying changes to AD object..." -ForegroundColor Cyan
try {
Set-ADObject -Identity $GPOPath -Server $AD -Replace @{ gPCMachineExtensionNames = $NewExtensions } -ErrorAction Stop
Write-Host "[+] gPCMachineExtensionNames updated successfully." -ForegroundColor Green
} catch {
Write-Host "[-] Failed to update AD object: $_" -ForegroundColor Red
Write-Host "[!] Restore the GPO using backup ID: $($Backup.Id) in $BackupPath" -ForegroundColor Yellow
exit 1
}
# STEP 7 - Verify
Write-Host "`n[*] Verifying applied value..." -ForegroundColor Cyan
$Verify = Get-ADObject -Identity $GPOPath -Server $AD -Properties gPCMachineExtensionNames
Write-Host "`n--- APPLIED state (gPCMachineExtensionNames) ---" -ForegroundColor Green
Format-ExtensionBlocks -ExtensionString $Verify.gPCMachineExtensionNames
Write-Host "`n[+] Done. Scheduled Task CSE extensions applied to '$GPOName'." -ForegroundColor Green
Write-Host "[+] Recovery backup available at: $BackupPath`n" -ForegroundColor Green
