Automate a Reboot or Custom Script When the Autopilot ESP is Complete

Introduction

Sometimes you end up discovering pretty neat things as a result of working on an unrelated issue. That’s how this post was born. I have been working on a way to rename hybrid AADJ devices during Autopilot to use their serial number for several weeks (I’ll have that post finished in a week or two). The main issue with the renaming a HAADJ device is triggering a reboot. Sure, we can do that manually, notify the user, or just flat out restart them whenever, but none of those options are very convenient. For this to really be conveniently, we need the reboot to occur right after the ESP is finished. This made me begin searching for some sort of file, registry key/value, or some kind of indication in the local OS that Autopilot ESP had completed. However, my internet searching kept coming up blank. This became something I needed to find an answer to. So, I went through everything I could think of to find this, and eventually found something I could use, but it’s not what I expected.

Solution Overview

This solution runs a custom PowerShell script (or scripts) right after the ESP completes. For this example, our custom script will do a few different small things, and then restart the device. There are two triggers during the ESP we rely on for this to happen. The first trigger kicks off a scheduled task that starts a script, and the second trigger is the indication that the ESP is finished, which then runs the rest of the script actions. I should warn you that Windows 10 and Windows 11 behave differently, so depending on your environment, you’ll need to target different scripts at Windows 10 than you do at Windows 11. We touch on Windows 10 at the end, but for the majority of this post, we assume you’re using Windows 11. I can also confirm this solution definitely works with Azure AD Join Autopilot devices. I have not thoroughly tested it yet with Hybrid-Joined devices, but as previously mentioned, I have another post coming soon that will cover Hybrid Autopilot and renaming during the ESP to use their serial number, which will use a similar (or the same) solution for the reboot. All scripts and files used in this post are available on my github. Let’s begin!

The ESP and the First Trigger

It’s important to know that at a very high level, the ESP is broken into two phases with the accounts that are used to perform tasks. Phase 1, which includes the Device Preparation and Device Setup uses the defaultuser0 account and the system account to perform various setup tasks. When the device moves into the Account setup phase, the defaultuser0 is signed out and the user account driving the autopilot process is authenticated and signed in using the PRT from the initial sign-in, which initiated the autopilot process. If you want detailed information on the phases and steps during the ESP, visit Rudy’s blog here – (Enrollment Status Page | Account Setup | Identifying | ESP (call4cloud.nl)).

There’s no secret way to push a custom script during the ESP. You need to use a Win32 app and make it required during the ESP. The issue is, what if you wanted to perform a reboot? You can make an app to reboot during the ESP, and it will do its best to pick back up where it left off, but you lose the PRT after rebooting and the user will need to sing in again after the reboot. So, we really don’t want to do this since it breaks the purpose of autopilot, which is to not babysit the device. Not to mention, you’d need to get creative with a detection rule for an app that simply reboots a device in the middle of the ESP. So, we will be using a Win32 app to accomplish this, but it will create a scheduled task which performs the rest of the steps. Scheduled tasks rely on triggers, so I needed to determine a trigger to start the task. That’s when I started monitoring services, processes, and other events that occurred during the ESP. Since ultimately, we want to execute our custom script right after the ESP finishes, we need a trigger that happens at some point during the account phase. After spending way too much time testing and researching this while monitoring autopilot, I found a suitable candidate. Remember, during the first two phases of the ESP, the user “defaultuser0” is performing tasks:

During the transition to the Account setup phase, defaultuser0 is signed out, and the user driving autopilot is signed in (using the PRT from when they initiated the autopilot process). We can see once we hit the account setup phase, the licensed user account is running processes:

During the transition from device setup to account setup phase, defaultuser0 is signed out, resulting in a security event with ID 4647. This is what I chose to use as my trigger to start the scheduled task:

The account setup phase is the final phase of the ESP. So, now that we have a trigger indicating the Account setup phase has begun, we can focus on our second trigger, which is trying to identify SOMETHING that indicates the ESP is finished. This will allow our script to perform additional tasks at the conclusion of the ESP. So, trigger 1 to start our scheduled task is event ID 4647. Next, we need to identify something indicating the ESP is complete.

The Second Trigger – Identifying when the ESP is finished

Everything on the internet tell us there are no files or reg values created that indicate a device has finished the ESP. After several days and many hours of testing, I can confirm this. There’s actually still a chance that something gets created somewhere, but this was like looking for a needle in a haystack. I needed to focus my attention on the account setup phase to determine what was happening and see if I could find any kind of trigger. Specifically, looking at things towards the end of the account setup phase. Event viewer and Windows Task Manager have limitations, and there is too much going on to accurately identify anything in real time. That’s when I got creative with ProcMon. To get ProcMon on my test autopilot device, I mounted the VHDX of my test device while it was powered off and copied the ProcMon files to the drive. Then dismounted and powered the device back on. We can see those files exist now before autopilot even begins, and we can launch it from CMD:

This allows us much more flexibility than task manager, which I was relying on until there was nothing blatantly obvious. As many of you know, ProcMon is MESSY if you don’t use filters. But, in this situation, I wanted to capture just about everything. I needed to know what was happening to see if I could find a trigger that the ESP was complete. That made for over 1.4 million lines of captured data to sift through, and that was only about 20 seconds worth. I specifically started the capture shortly before the ESP completed, and shortly after the desktop appeared. This would give me around 20-25 seconds to figure out if there was any file, registry key, process, or ANY indication that the ESP was done, and something we could trigger a task from.

I was not able to identify any specific registry keys or values, or any files created at the conclusion of the ESP, so I turned all my attention to processes. After applying some filters to the Procmon output, I found a bunch of process activity roughly 10 seconds before my user desktop appeared and the ESP concluded. Two processes stopped at this time – appidpolicyconverter and conhost. I started focusing in on these processes. Remember, this is for Windows 11. Windows 10 does not behave this way. I’ll touch on Windows 10 at the end.

Both of these processes were very short lived. The appidpolicyconverter process starts 1.6 seconds before it stops. It actually calls the conhost process, which only runs for around 0.5 seconds before exiting. I re-ran this on another Windows 11 machine to confirm and got the same results. At this point, my focus is now on the appidpolicyconverter process.

So, what is the appidpolicyconverter.exe process? Well, details are pretty slim. Procmon output doesn’t show this process creating or deleting anything. Mostly just querying files and reg values and loading several .dll files. Some searching online says this process is related to app locker and software restriction policies. I also found that there is a scheduled task to trigger this process.

I may do some additional digging into this process at a later date, but for now, I had what I was looking for. I wiped and reloaded two other test devices with Windows 11 22H2 to confirm this same process behavior to make sure this wasn’t a fluke. Now that we have our second trigger, we can focus on creating our task and post ESP script.

The Task and Scripts to Perform Post-ESP actions

We need to do two things – Make our Win32 app that deploys a scheduled task with a trigger for logoff event ID 4647, and write our script we want to execute as soon as the ESP completes (which is essentially when the appidpolicyconverter process exits), and disable the scheduled task after.

The Scheduled Task

All of these files are available on github, and I also have a detailed post on deploying scheduled tasks with Intune here. You can also use PowerShell to create a scheduled task if you’d prefer to do it that way. Regardless, here is our scheduled task XML file and some screenshots of the task after its deployed:

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2023-03-20T05:53:00.4766991</Date>
    <Author>SMBtotheCloud</Author>
    <URI>\PostESP-Script</URI>
  </RegistrationInfo>
  <Triggers>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Security"&gt;&lt;Select Path="Security"&gt;*[System[EventID=4647]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <UserId>S-1-5-18</UserId>
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>false</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
    <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>powershell.exe</Command>
      <Arguments>-executionpolicy bypass c:\temp\PostESP-Script.ps1</Arguments>
    </Exec>
  </Actions>
</Task>

You’ll see in the XML, I have it referencing c:\temp\postesp-script.ps1 as the action to take when triggered. Our installation script will copy that file locally. If you don’t want to use c:\temp then change the directory to something else and adjust the install script accordingly. We can see in the screenshot below that the task is disabled – this is only because it has already run on this device. But we can see our trigger for event 4647. Remember, the task will get deployed during the device phase of the ESP, the next time event 4647 happens is when the defaultuser0 account is signed out, and ESP moves to the Account phase. That’s when this task gets triggered.

And our install script we use for our Win32 app is below. It’s fairly basic since this really isn’t doing much other than creating the temp directory, staging our script file in the temp directory, and then creating the scheduled task.

$tempdir = "c:\temp"
New-Item $tempdir -ItemType Directory -Force
Copy-Item ".\PostESP-Script.ps1" -Destination $tempdir -Force
Register-ScheduledTask -xml (Get-Content '.\PostESP-Script.xml' | Out-String) -TaskName "PostESP-Script" -Force

The Post ESP script:

Next, let’s examine our action script when the task executes. Remember, the scheduled task will run at the conclusion of the Device phase of the ESP, so it’s essentially starting right when the account phase begins. However, our appidpolicyconverter process doesn’t start until the end of the ESP and only runs for 1-2 seconds. So, this is what I came up with for the reboot script at the end of the ESP:

  • The main purpose of the script is to execute a reboot. When the script kicks off, it waits for the appidpolicyconverter process to start. It checks every 150 milliseconds. Once the process starts, it waits until that process ends, and then disables the scheduled task, waits 5 seconds, and then performs a reboot.
  • Logs all output from powershell to c:\temp\post-esp-task.txt
  • Also creates a dummy ESP-TaskComplete file in c:\windows\system32\tasks. I did this in case you wanted to fully delete this scheduled task after it runs, we can use a custom detection script that detects this app was successful by looking for that file.

Here’s our Post-ESP action script:

$tempdir = "C:\temp"
$taskdir = "C:\windows\system32\Tasks"
$proc = "appidpolicyconverter"
New-Item $tempdir -ItemType Directory -Force
New-Item $taskdir -ItemType File -Name ESP-TaskComplete -Force
Start-Transcript -Path "C:\temp\post-esp-task.txt" -Verbose
start-sleep -Seconds 5
Write-Host "Waiting for process to start..."
while ($true) {
    $getprocess = Get-Process $proc -ErrorAction SilentlyContinue
    if ($getprocess -ne $null) {
        Write-Host "$proc has started."
        Wait-Process -Name $proc -Verbose
        break
    }
    Start-Sleep -Milliseconds 150
}
Write-Host "Process has ended. Restarting Workstation" -Verbose
disable-scheduledtask -taskname PostESP-Script -ErrorAction SilentlyContinue -Verbose
start-sleep -Seconds 5
Restart-Computer -Force -Verbose

And this is the custom detection script. Note that we are checking for the scheduled task file, OR the “ESP-TaskComplete” file we created in the previous script. If disabling the scheduled task isn’t enough for you and you want to delete it, uncomment the line in the custom detection script to unregister the scheduled task if it exists πŸ™‚

$PostESPTask = Test-Path "C:\windows\system32\tasks\PostESP-Script"
$PostESPFile = Test-Path "C:\windows\system32\tasks\ESP-TaskComplete"
#Unregister-ScheduledTask -TaskName PostESP-Script -ErrorAction SilentlyContinue
If (($PostESPTask -eq $true) -or ($PostESPFile -eq $true)) {
    Write-Host "Detected"
    Exit 0
}
else {
    Exit 1
}

Deployment and Example

In order to deploy this, we really have no choice but to use a Win32 app. We can make this a required application during the ESP, but it needs to be assigned to a device group so it gets deployed during the device setup phase. I’m not going back through all the steps to package this as a Win32 app for deployment, but as previously mentioned, my other post goes into detail about deploying scheduled tasks with Win32 apps – Deploy Scheduled Tasks as Win32 Apps – SMBtotheCloud.

Here’s a sped-up GIF showing a device going through the ESP and rebooting at the conclusion. This is the user experience you can expect to see:

What about Windows 10?

Windows 10 does not exhibit the same process behavior near the completion of the ESP account phase. I did not do nearly as much testing and validation with Windows 10 since its being removed from almost every environment I work with, but I was able to get this working by monitoring the cloud experience host broker process (wwahost.exe). This process seemed to run throughout the entire ESP, but then ended 5-10 seconds AFTER the user received their desktop, which then auto-reboots the computer. I did test this a few times with success, but I recommend you do your own testing to verify this behaves how you want. The version of Windows 10 I used for testing was 21H2. I used the same install script and scheduled task for Windows 10, but our post-esp.ps1 script is different. That is below, and I have this separated on github.

$tempdir = "C:\temp"
$taskdir = "C:\windows\system32\Tasks"
New-Item $tempdir -ItemType Directory -Force
New-Item $taskdir -ItemType File -Name ESP-TaskComplete -Force
Start-Transcript -Path "C:\temp\post-esp-task.txt"
start-sleep -Seconds 15
write-output "Waiting for the cloud experience host broker process to complete..."
wait-process "wwahost" -Verbose
Write-Output "Process ended. Restarting Workstation"
disable-scheduledtask -taskname PostESP-Script
Restart-Computer -Force

Here’s the user experience for Windows 10. You’ll see that there are 2-5 seconds where the user’s desktop will appear after the ESP is finished before the cloud experience host broker process ends, which triggers the reboot:

That’s all for this post!

2 thoughts on “Automate a Reboot or Custom Script When the Autopilot ESP is Complete”

  1. Hi Gannon,

    Great blog about rebooting the device β€œafter” finishing the ESP. I did some tests on Windows 11 and the reboot will occur if the process appidpolicyconverter is stopped. During my tests, the reboot did occur during the account setup fase. I played with the start-sleep timer (set to 300) and than the ESP had enough time to finish. So timing is still essential!.

    Also, the Windows 11 powershell post-esp scripts on Gitgub is not equal to this blog ;-). On GitHub the Windows 10 and Windows 11 powershell post-esp scripts are identical.

    1. Hey Erwin! Thanks for the heads up on the github scripts. I must have mistakenly uploaded the Windows 10 script for Windows 11. I just made that correction :). Also, regarding the timing. I definitely agree. I suggest testing like you did before putting this into production. Hardware and other variables may affect the timing of the restart. Thanks for reading and commenting!

Comments are closed.