Search code examples
powershellscheduled-taskswindows-task-scheduler

PowerShell query Windows task scheduler for tasks that will run between specific datetimes


I've inherited a server that runs Windows scheduled tasks for several hundred various processes (mostly it kicks off custom PowerShell scripts) - the schedules for these tasks can be as frequently as every 15 minutes, or as infrequently as once per year on a specific date.

How can I query task scheduler using PowerShell (or really any other means also) to determine what if any tasks will be running between a specific date-time range in the future? This is necessary so we can for example schedule and perform maintenance on this server, and others that it interacts with.


Solution

  • I should start out by mentioning this is a deceptively complex topic. There are few reasons for this, including but not limited to:

    1. Scheduled Tasks as objects can be very complex with widely variable schedules, repetition intervals, other trigger types, and a plethora of other configuration points.
    2. The non-GUI, meaning CLI, cmdlet, and/or API tools have changed over the years. And, it's not always easy to string them together to solve the complexities cited in point 1.

    WARNING: This report is going to be very tough to write. Before you embark on a challenge like this you should look for any preexisting tools that may inventory or lend other transparency to the Task Scheduler.

    Also, it may be better to define the maintenance window as a matter of policy. Then you can configure jobs to avoid the window. At a glance, this strikes me as much easier than writing a difficult program to assist in the cherry-picking of a time slot.

    If you choose to move forward here are a few tools that may help:

    1. Schtasks.exe: A surprisingly capable CLI tool that's been around a long time.
    2. The TaskScheduler PowerShell module: A PowerShell module written around scheduling COM objects. It was originally part of the Windows 7 PowerShell pack, but these days you can get it through the PowerShell Gallery. The easiest way is through typical module cmdlets like Find-Module & Install-Module
    3. The newer ScheduledTasks module installed by default on later Windows systems. It's Written around Cim (WMI) and is installed by default in later versions of PowerShell / Windows.

    CAUTION: It's easy to confuse the latter 2 tools. Not only are the names are very similar, but there are overlap and similarity between the commands they contain. For example, both modules have a Get-Scheduledtask cmdlet. Two ways to deal with the confusion:

    1. When importing a Module with the Import-Module cmdlet use the -Prefix parameter to add a prefix to the none part of the cmdlet. then use that prefix when calling the cmdlet thereafter.
    2. Call cmdlets with a qualified name like TaskScheduler\GetScheduledTasks

    Now to get at the scheduling data. In my experience, the needed details are only exposed through the task's XML definition. Again, you're going to bump up against the complexity that comes with a wide range of scheduling and/or trigger options. You can read about the XML schema here

    Here are some examples of how to access the XML data:

    schtasks.exe /query /s pyexadm1 /tn <TaskName> /XML
    

    In this case, you'll have to do additional string manipulation to isolate the XML then cast it to [XML] so you can work with it in a typical PowerShell manner. Obviously, there will be challenges to leveraging this tool more broadly. However, it's very handy to know for quick checks and work, especially where the next tool is not immediately available.

    Note: if you don't cite the /TN argument all tasks will be returned. While the next method is easier, it's good to know this approach, it will be handy while you are developing.

    The next example uses the older TaskScheduler module (#2 above):

    $TaskXML = [XML](TaskScheduler\Get-ScheduledTask -ComputerName <ComputerName>-Name <TaskName>).XML
    

    Note: Above assumes no prefix was used. So, you must cite the source module to prevent confusion with the ScheduledTask module.

    This example loads the XML text and converts it to an XmlDocument object in a single line. Then you can access data about the task like below:

    $TaskXML.Task.Triggers.CalendarTrigger
    

    This may yield output like:

    StartBoundary       Enabled ScheduleByWeek
    -------------       ------- --------------
    2020-09-14T08:00:00 true    ScheduleByWeek
    

    You can run this in mass by leveraging the pipeline, which might look something like below:

    $XMLTaskData = 
    TaskScheduler\Get-ScheduledTask -ComputerName <ComputerName> -Recurse | 
    ForEach-Object{ [XML]$_.XML }
    

    In the above pipeline example the resulting $XMLTaskData is an array each element of which is a respective XML task definition.

    Note: Use of the -Recurse switch parameter. Given the high number of tasks, I wouldn't be surprised if they were organized into subfolders.

    Similarly, you can also use the Export-ScheduledTask cmdlet from the ScheduledTasks module:

    $TaskXML = [XML](Export-ScheduledTask -CimSession <ComputerName> -TaskName <TaskName>)
    

    And you can leverage the pipeline like this:

    $XMLTaskData = 
    Get-ScheduledTask -CimSession <ComputerName> | 
    Export-ScheduledTask | 
    ForEach-Object{ [XML]$_ }
    

    Like the other piped example, this results in an array of XML task definitions.

    Note: In this case, there is no -Recurse parameter. You can specifically cite paths though.

    With any of these approaches you obviously need some familiarity with working with XML objects in PowerShell, but there are tons of tutorials or other resources for that.

    Again, the complexity here is in dealing with many trigger types and scheduling paradigms. On your road to getting a Minimally Viable Program (MVP), you may want to use these techniques to inventory the existing tasks. That can help you prioritize your development process.

    A final point; knowing when a task is going to run may be quite different than know when it's running. For example, a task may run a 1:00 PM, but the duration of the job is variable and unaccounted for. That strikes me as very difficult to contend with. You may need another procedure to look for task completion events in the event logs. You may also need to consider execution time limits which can be found in the XML data.