Self-Deleting Scheduled Task via PowerShell

Starting with PowerShell v4, there has been cmdlets available to create scheduled tasks instead of using the schtasks.exe program, or the wizard.

My organization is using PowerShell v5.1 and there is a lingering bug from the Windows Vista days relating to setting an “EndBoundary” in order to have a scheduled task expire.  An expiration date/time is needed for the scheduled task to delete itself.  But, if you try to set this using the cmdlets, you get an error message.

This post is a culmination of tidbits I found on several other blog/forum posts, and a metric crap-ton of my own trial and error.  All because I wanted a scheduled task to delete itself once it was done.  That doesn’t seem like much, does it?  Yeah, I didn’t think so either, but it was.  My pain is your gain.

I start by creating the scheduled task via cmdlets with as many items configured as I can without running into errors that snowball.

There is a “DeleteExpiredTaskAfter” parameter that is part of the “New-ScheduledTaskSettingsSet” cmdlet.  In order to use this parameter, you need to set an “EndBoundary” date/time to the trigger.  If you look at the “New-ScheduledTaskTrigger” cmdlet, there is no parameter for setting the EndBoundary. If you try to create the scheduled task with “DeleteExpiredTaskAfter” set, you get an error.  Removing the parameter allows the script to run successfully without error. Ugh.

I wanted the following to happen:

  • Run the “Machine Policy Evaluation Cycle”
  • Have it run every hour for 24 hours
  • Have the scheduled task delete itself when complete

To get this to happen, I need to do the following:

  1. Create the scheduled task without the “DeleteExpiredTaskAfter” parameter
  2. Load the scheduled task into a variable
  3. Make tweaks to the variable (like “DeleteExpiredTaskAfter”, the boundary dates/times, and some others)
  4. Apply tweaked settings back to the scheduled task

Please note there is liberal use of the backtick (`) as a line break. Normally, I would have a block of variables at the top which would be used in the lengthy commands later. I also tried splatting, but that requires a “parameter = value” format and some of the cmdlet parameters don’t have values, therefore, splatting didn’t work right. To keep the script short and for ease of reading and maintenance, I opted for the not so recommended use of the backtick (or grave).

With that out of the way, breaking it down, you can see that I set the individual cmdlets to variables for use with the “Register-ScheduledTask” cmdlet. This was done, again, for ease of reading and maintenance.

1
2
3
4
# Set up action to run
$STAction = New-ScheduledTaskAction `
-Execute 'wmic.exe' `
-Argument '/namespace:\\root\ccm path sms_client CALL TriggerSchedule "{00000000-0000-0000-0000-000000000022}" /NOINTERACTIVE'

The action I want to run is the command-line execution of the WMIC command that launches the “Machine Policy Evaluation Cycle” that will check in with the SCCM server to see if there are any applications or packages that are required/available. These items can appear at various times because other evaluations are run at different intervals and sometimes there are collections that a device can move in and out of based on certain criteria (installed programs, certain configurations, etc.) and those can all take time to process. Thus, the frequent check-ins.

1
2
3
4
5
6
# Set up trigger to launch action
$STTrigger = New-ScheduledTaskTrigger `
-Once `
-At ([DateTime]::Now.AddMinutes(1)) `
-RepetitionInterval (New-TimeSpan -Hours 1) `
-RepetitionDuration (New-TimeSpan -Days 1)

The trigger is set to run once at a time of “Now + 1 Minute”, this gives plenty of time for the script to make the post-creation tweaks before the task actually runs. The trigger is also set to run every hour for 1 day.

1
2
3
4
5
6
7
8
# Set up base task settings - NOTE: Win8 is used for Windows 10
$STSettings = New-ScheduledTaskSettingsSet `
-Compatibility Win8 `
-MultipleInstances IgnoreNew `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-Hidden `
-StartWhenAvailable

The settings tell the task to run right away, even if the device is off AC power. There is no Windows 10 compatibility setting, you use “Win8” which covers it.

1
2
3
4
5
6
7
8
9
10
11
12
# Name of Scheduled Task
$STName = "_Newly Imaged Machine"

# Create Scheduled Task
Register-ScheduledTask `
-Action $STAction `
-Trigger $STTrigger `
-Settings $STSettings `
-TaskName $STName `
-Description "Executes Machine Policy Retrieval Cycle every hour for 24 hours" `
-User "NT AUTHORITY\SYSTEM" `
-RunLevel Highest

The scheduled task references the previously set variables (Action, Trigger, and Settings) and is then set to run as “SYSTEM” along with a name and description.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Get the Scheduled Task data and make some tweaks
$TargetTask = Get-ScheduledTask -TaskName $STName

# Set desired tweaks
$TargetTask.Author = 'iamsupergeek'
$TargetTask.Triggers[0].StartBoundary = [DateTime]::Now.ToString("yyyy-MM-dd'T'HH:mm:ss")
$TargetTask.Triggers[0].EndBoundary = [DateTime]::Now.AddDays(1).ToString("yyyy-MM-dd'T'HH:mm:ss")
$TargetTask.Settings.AllowHardTerminate = $True
$TargetTask.Settings.DeleteExpiredTaskAfter = 'PT0S'
$TargetTask.Settings.ExecutionTimeLimit = 'PT1H'
$TargetTask.Settings.volatile = $False

# Save tweaks to the Scheduled Task
$TargetTask | Set-ScheduledTask

Now that the scheduled task is created, it is pulled into a variable for tweaking. NOW the elusive EndBoundary can be set along with some other items that didn’t have cmdlet parameters available… or were available, but could not be set without error, like “DeleteExpiredTaskAfter” which started this whole mess.

Notice that the Triggers are referenced using array numbers. This scheduled task only has one trigger, so we tweak number [0]. It’s also important to note that some of the settings have pre-configured values that are allowed. If you try to set a different value, an error will be thrown.

Examples: PT0S = Immediately (0 seconds). PT1H = 1 Hour. These match pull-down options within the scheduled task. You can only use the options available from the pull-down, if you try to set these types of values for something else, like say “PT5S” for 5 seconds, an error message will be thrown.

Once the tweaks are made, apply them to the scheduled task for use via the “Set-ScheduledTask” cmdlet.

Here’s the whole script put together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Set up action to run
$STAction = New-ScheduledTaskAction `
-Execute 'wmic.exe' `
-Argument '/namespace:\\root\ccm path sms_client CALL TriggerSchedule "{00000000-0000-0000-0000-000000000022}" /NOINTERACTIVE'

# Set up trigger to launch action
$STTrigger = New-ScheduledTaskTrigger `
-Once `
-At ([DateTime]::Now.AddMinutes(1)) `
-RepetitionInterval (New-TimeSpan -Hours 1) `
-RepetitionDuration (New-TimeSpan -Days 1)

# Set up base task settings - NOTE: Win8 is used for Windows 10
$STSettings = New-ScheduledTaskSettingsSet `
-Compatibility Win8 `
-MultipleInstances IgnoreNew `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-Hidden `
-StartWhenAvailable

# Name of Scheduled Task
$STName = "_Newly Imaged Machine"

# Create Scheduled Task
Register-ScheduledTask `
-Action $STAction `
-Trigger $STTrigger `
-Settings $STSettings `
-TaskName $STName `
-Description "Executes Machine Policy Retrieval Cycle every hour for 24 hours" `
-User "NT AUTHORITY\SYSTEM" `
-RunLevel Highest

# Get the Scheduled Task data and make some tweaks
$TargetTask = Get-ScheduledTask -TaskName $STName

# Set desired tweaks
$TargetTask.Author = 'IT Support'
$TargetTask.Triggers[0].StartBoundary = [DateTime]::Now.ToString("yyyy-MM-dd'T'HH:mm:ss")
$TargetTask.Triggers[0].EndBoundary = [DateTime]::Now.AddDays(1).ToString("yyyy-MM-dd'T'HH:mm:ss")
$TargetTask.Settings.AllowHardTerminate = $True
$TargetTask.Settings.DeleteExpiredTaskAfter = 'PT0S'
$TargetTask.Settings.ExecutionTimeLimit = 'PT1H'
$TargetTask.Settings.volatile = $False

# Save tweaks to the Scheduled Task
$TargetTask | Set-ScheduledTask

I don’t think the backticks are really that much of a problem here. PowerShell purists will disagree and that’s OK. The script looks clean, it is easy to read, and can be maintained without too much trouble. Just make sure that there is a space before the backtick (`) character and nothing after it (not even #comments!). Also, the last line of the command should not have a backtick (an error will be thrown).

On my test box, I noticed that the task wasn’t deleting out of the schedule task list as it was configured to do. Apparently, they only delete from the list after a reboot. In testing, I had an expired task sit in the list for several days and it only left the list when the box was rebooted. ¯\_(ツ)_/¯

This scheduled task is part of a process that we implemented at my organization for newly imaged devices. They are put into a collection for 24 hours which has an open maintenance window and faster check-in times with SCCM and other systems to get them up to speed before deployment. Future blog posts will detail those specifics. Stay tuned!

As always, make sure you test (and understand) any and all code you get from the internet in a test environment before using it in production. If you hose something up, it is not my fault!

Comments ( 7 )

  1. Jerome C.
    Damn, tha'ts exactly what i was looking for. Thanks for all the pain you endured to get it working !
  2. Hervé
    Thanks A LOT for sharing. Works like a charm
  3. Gundares
    I have got this one working for me, only modifying the TASK object before registering it: $Task = New-ScheduledTask -Action $Task_Action -Trigger $Task_Trigger $Task.Triggers[0].EndBoundary = $trg_expr #some datetime that is ahead of trigger's exec time $Task.Triggers[0].ExecutionTimeLimit = "PT30M" # "PT0S" did not work in the below line $Task.Author = $User $Task.Settings = (New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Seconds 0)) Register-ScheduledTask -TaskName $Task_Name -TaskPath $Task_Root -InputObject $Task -User $User -Password $UnsecurePassword I needed a task that is executed as a normal user, not "SYSTEM", this is why -Password is required while registering it.
  4. HeyBuddy
    Thank you for your great sacrifice!!!
  5. Mandy
    Just Awesome :)
  6. Brent W.
    Cool answer, thank you for posting it. I was able to improve it a little bit on the trigger object. I am not sure if this works for the version you built this article for, I am using PSv5.1. After defining the trigger, you can modify the "EndBoundary" before registering the task, with: $STTrigger.EndBoundary = "{0:yyyy-MM-dd'T'HH:mm:ss}" -f (get-date).AddHours(3) This allows you to use the "-DeleteExpiredTaskAfter" on the SettingsSet object without errors. This removes the need to get/modify the scheduled task after registering it.
  7. Thomas Braun
    I just found out that there is no need to reboot the computer to get rid of the deleted task. A restart of the task scheduler services does the trick as well: restart-service -name schedule