Create an idle trigger in Power­Shell for Windows Task Scheduler

I’ve found quite a few articles and examples demonstrating how to schedule tasks in the Windows Task Scheduler using PowerShell that will trigger only when the computer is idle. They all have one thing in common: The instructions don’t work as the authors have blindly followed the [mostly incorrect] documentation without verifying that the tasks work as intended. Many of the examples never execute their tasks at all and others execute them at the wrong time.

Here is my attempt at a more palatable recipe for scheduled tasks that execute while the computer is idle yet stays clear of the minefield of bugs that plagues the aging Task Scheduler in Windows.

Firstly, be warned that the graphical user interface for the Task Scheduler appears misleadingly easy to configure in the most recent versions of Windows. There are many requirements and conditions that will make a task trigger only once or never at all even though the task appears to have been set up correctly.

The Task Scheduler has gained quite a lot of features over the years without receiving sufficient testing. Always carefully test your scheduled tasks and their repetition patterns! Many of the parameters you configure in the GUI don’t work in some combinations or have requirements that the GUI doesn’t provide feedback for. Adding insult to injury, the feedback that’s provided by the GUI is often incorrect.

The Task Scheduler cmdlets for PowerShell either don’t expose required parameters or don’t allow you to combine parameters that are required for many of the common task triggers. The idle and event triggers aren’t even settable using the cmdlets despite parameters for those two triggers being exposed. Frankly, the developers at Microsoft who made the cmdlets don’t appear to have been aware of how the Task Scheduler works.

The result is a collection of cmdlets that can make a few basic tasks, but aren’t nearly as powerful as they appear at first. Testing is the key to success with the Windows Task Scheduler; you can’t blindly rely on it doing what you tell it to.

Let us look at the forth New-ScheduledTaskSettingsSet example on MSDN:

$Sta = New-ScheduledTaskAction -Execute "Cmd"
$Stset = New-ScheduledTaskSettingsSet -RunOnlyIfIdle -IdleDuration 00:02:00 -IdleWaitTimeout 02:30:00
Register-ScheduledTask Task01 -Action $Sta -Settings $Stset

It looks simple, right? Unfortunately, this example task wouldn’t ever execute and it has multiple issues. There’s no actual trigger associated with the task, so it wouldn’t ever be executed. The idle trigger isn’t even exposed through the cmdlets so you can’t correct it.

The Microsoft developers who made this possibly envisioned that setting the -RunOnlyIfIdle parameter should automatically create an idle trigger; but this was never implemented. As a user, you can work around it by invoking a CIM instance of an idle trigger and adding it as a property to the Register-ScheduledTask object:

$Sta = New-ScheduledTaskAction -Execute "Cmd"
$Stset = New-ScheduledTaskSettingsSet -RunOnlyIfIdle -IdleDuration 00:02:00 -IdleWaitTimeout 02:30:00
$Sttrg = (Get-CimClass -ClassName 'MSFT_TaskIdleTrigger' -Namespace 'Root/Microsoft/Windows/TaskScheduler')
Register-ScheduledTask Task02 -Action $Sta -Settings $Stset -Trigger $Sttrg

The task still won’t be executed when it should because the -RunOnlyIfIdle parameter isn’t being set. It will always be set to false when you register the task! This gives further credence to my theory that a Microsoft employee somewhere had great plans for how this parameter should work, but never finished the work. In any case, the parameter is required to be set to true but that can’t be achieved using the PowerShell cmdlets.

After registering the above example task with the added idle trigger in Task Scheduler, you could update the task over the COM interface to switch the RunOnlyIfIdle parameter to true by bypassing the broken cmdlets. If you did this you’d have a task that respects the set IdleDuration only for the first time the task is executed, and would thereafter always execute the task after the computer has been idle for four minutes. (The value of four minutes is a hardcoded default in Windows.) This is the first issue we’ve run into so far that isn’t the cmdlets’ fault, but this is a problem that appears to have been introduced by the new “unified task scheduler engine” in Windows 8. You can’t work around that one.

The Task Scheduler cmdlets appear to be quite broken. (The alternative ScheduledJob cmdlets provide a slightly different interface and somewhat higher quality, but they too are quite broken.) I’ve focused on creating an idle task in this article, but there are plenty more problems like the above if you sit down and inspect what the cmdlets say they’re doing versus what they’re doing. Time for a different approach!

The quite handy-looking cmdlets provided with PowerShell 4 and newer wouldn’t get the job done. Luckily, PowerShell offers alternative ways to accomplish nearly any task. Before PowerShell 4 introduced these new Task Scheduler cmdlets, the only way to interact with the Task Scheduler was over its COM interface. Next, I’ll reimplement the intent of MSDN’s Example 4 (see the first code block earlier in the article) using that interface instead of the purpose-written cmldets.

This will register a task that executes calc (the Windows calculator) every 40 minutes after the computer has been idle for at least 15 minutes:

# SPDX-License-Identifier: CC0-1.0

$TaskName = "Task03"

$service = New-Object -ComObject("Schedule.Service")
$rootFolder = $service.GetFolder("")

$taskdef = $service.NewTask(0)

# Creating task settings with some default properties plus
# the task’s idle settings; requiring 15 minutes idle time
$sets = $taskdef.Settings
$sets.AllowDemandStart = $true
$sets.Compatibility = 2
$sets.Enabled = $true
$sets.RunOnlyIfIdle = $true
$sets.IdleSettings.IdleDuration = "PT15M"
$sets.IdleSettings.WaitTimeout = "PT40M"
$sets.IdleSettings.StopOnIdleEnd = $true

# Creating an reoccurring daily trigger, limited to execute
# once per 40-minutes.
$trg = $taskdef.Triggers.Create(2)
$trg.StartBoundary = ([datetime]::Now).ToString("yyyy-MM-dd'T'HH:mm:ss")
$trg.Enabled = $true
$trg.DaysInterval = 1
$trg.Repetition.Duration = "P1D"
$trg.Repetition.Interval = "PT40M"
$trg.Repetition.StopAtDurationEnd = $true

# The command and command arguments to execute
$act = $taskdef.Actions.Create(0)
$act.Path = "calc"
$act.Arguments = "/?"

# Register the task under the current Windows user
$user = [environment]::UserDomainName + "\" + [environment]::UserName
$rootFolder.RegisterTaskDefinition($TaskName, $taskdef, 6, $user, $null, 3)

The Task Scheduling Scripting Objects aren’t nearly as neat and compact as the Task Scheduler cmdlets, but they are described in detail on MSDN. The documentation is informal but doesn’t make any mentions of which properties can be combined and only acknowledges a handful of the dependencies, and limitations and requirements for each property. As always when working with the Task Scheduler: test and verify everything.

What you’ve got now is a “daily” task trigger (from the constant in Triggers.Create(2)) defined to execute every day by the interval parameter; that’s turned into a 40-minute trigger by setting the repetition parameter. (The values for these two parameters are ISO-8601 formatted durations, if you didn’t recognize them.)

The task is then told to only execute if the machine has been idle for at least 15 minutes by the IdleSettings.IdleDuration parameter. Note that the task’s repetition parameter should be repeated in the WaitTimeout parameter.

The only way to execute a task only once after the user has gone idle is to use the idle trigger, but then you lose control over IdleSettings.IdleDuration as the trigger is always fired after 4 minutes of inactivity. The documentation, as well as the graphical user interface, makes it completely clear that it’s supposed to work, but it seems to have stopped working after Windows 8.

Make sure to thoroughly test that your task behaves as you want it to! There goes a lot of parameters into a scheduled task. Just because you don’t get an error and the graphical user interface for the Task Scheduler appears to reflect the options you want doesn’t mean you’ve got a working task doing what you expect it to.

The GUI is deceptively easy to misconfigure. For example, avoid combing multiple triggers in one task when you set the IdleSettings.IdleDuration parameter. It will lead to unexpected behavior for both triggers. For example, the at login trigger will wait for the computer to be idle for exactly four minutes rather than execute immediately after login when used in combination with an idle trigger. There are less obvious situations like this all throughout Task Scheduler.

Oh, lastly I’d like to add that the Task Scheduler in Windows 10 stinks. It’s various APIs and front-ends are showing clear signs of code rot and decay from decades of steady feature creep, no clear planning, a complete lack of quality control and testing. It shouldn’t be relied upon and ought to be avoided if at all possible. 💩