Powershell - Monthly Scheduled Task Trigger - powershell

I'm currently automating the creation of scheduled tasks via Powershell, and I'm using the New-ScheduledTaskAction, New-ScheduledTaskTrigger, and Register-ScheduledTask commands. Now, I have a few jobs that need to run under the following conditions :
Run once every Friday
Run once on the 1st of every month
Run once on the 10th day of every month
While the technet documentation for the New-ScheduledTaskTrigger command specifies a Daily and Weekly time span parameter, I do not see one for Monthly, which is critical for defining the run times above.
In the few years I've been using Powershell, I can't think of an instance where I could do something via the standard UI interface, that I couldn't accomplish using one of the available commands.
Is this just flat out not available here, or am I missing something?
UPDATE #1
I just stumbled upon this SuperUser question, which does look promising, but references PSV3 instead of PSV4 - going to give it a shot and report back.

As I said in the original post, the SuperUser question above looked promising, but ultimately did not work with PSV4, and the example given in the post was basically a copy\paste job with almost no context.
I realized I could leverage Schtasks.exe from my Powershell script to handle the monthly aggregations, and it's fairly easy to set up, albeit somewhat tedious :
# set the trigger depending on time, span, and day
$runSpan = $task.SpanToRun;
if ($runSpan.Equals("Daily"))
{
$trigger = New-ScheduledTaskTrigger -Daily -At $task.TimeToRun
Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $task.TaskName -User $Username -Password $Password -Description $task.Description
}
if ($runSpan.Equals("Weekly"))
{
$trigger = New-ScheduledTaskTrigger -Weekly -At $task.TimeToRun -DaysOfWeek $task.DayToRun
Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $task.TaskName -User $Username -Password $Password -Description $task.Description
}
# script out SchTasks magic to create the monthly tasks
if ($runSpan.Equals("Monthly"))
{
$taskParams = #("/Create",
"/TN", $task.TaskName,
"/SC", "monthly",
"/D", $task.DayToRun,
"/ST", $task.TimeToRun,
"/TR", $filePath,
"/F", #force
"/RU", $Username,
"/RP", $Password);
# supply the command arguments and execute
#schtasks.exe $taskParams
schtasks.exe #taskParams
}
I'm using an in-script class to keep track of all the task properties ($task.TimeToRun, $task.DayToRun, etc.), iterating over a list of those, applying the Powershell implementation for daily and weekly tasks, then switching to SchTasks.exe for the monthly spans.
One thing I want to note, is that at first glance, I thought setting the user context under which the task runs could be achieved with the U and P arguments, but that is not the case. That specifies the creds that Schtasks.exe runs under - in order to set the user context for the task, you must use RU and RP.
In addition to the link above, these two were also very helpful :
http://coding.pstodulka.com/2015/08/02/programmatically-scheduling-powershell-scripts-in-task-scheduler/
https://msdn.microsoft.com/en-us/library/windows/desktop/bb736357(v=vs.85).aspx

Related

Specify location with Task Scheduler script

I'm trying to write a PowerShell script that will activate another script on a daily basis. What I have is:
$Sta = New-ScheduledTaskAction -Execute "powershell.exe" -argument "C:\Users\UserName\Desktop\Script.ps1"
$Stt = New-ScheduledTaskTrigger -Daily -At 3am
Register-ScheduledTask Task1 -Action $Sta -Trigger $Stt
It's not working because it can't find the file. PowerShell's default directory is the user's root directory, so I don't know if that's it, even though I've tried using the full path to the file? Or is it due to where the scheduled task is running from? Location says \ and I cannot seem to change that, even in the task scheduler app. I've been Googling for days and I can't figure it out. Anyone have any idea? I'm working on Windows 10 Pro.
Soufflegirl13,
Updated & Tested:
$TaskSet = New-ScheduledTaskSettingsSet -Compatibility Win8
$TaskSet.DisallowStartIfOnBatteries = $True
$TaskSet.StopIfGoingOnBatteries = $True
$TaskSet.IdleSettings.StopOnIdleEnd = $false
#Adjust the following two lines to meet your requirements
$TaskDesc = "Retrieve the status of HyperV"
$TaskName = "Task1 - or whatever you want here"
$Stt = New-ScheduledTaskTrigger -Daily -At 3am
#Adjust the -file parameter to point to your script
$TAArgs = #{Execute = '"' + "c:\windows\system32\windowspowershell" +
"\v1.0\powershell.exe" + '"' ;
Argument = "-file `"G:\BEKDocs\Scripts\Misc\Get-HyperVStatus-V-3.ps1`"" }
$TaskAction = New-ScheduledTaskAction #TAArgs
$RSTArgs = #{Action = $TaskAction
TaskName = "$TaskName"
Trigger = $Stt
User = "$env:USERDOMAIN\$env:username"
RunLevel = "Highest"
Description = "$TaskDesc"
Settings = $TaskSet}
Register-ScheduledTask #RSTargs
I've found that forcing the quote marks around the powershell filespec solved some problems. You might want to try it around the script name if the previous doesn't work.
Of course adjust any settings as you see fit to meet your requirements. Also I've not used this basic code to set a trigger before but I adjusted it to do so according to the documentation for Register-ScheduledTask so it should work.
Update Notes:
I checked the documentation for the New-ScheduledTaskSettingsSet cmdlet and it doesn't include Win10 as a valid value for Compatibility, go figure, so I backed it off to Win8. You can also just not use the Compatibility argument and accept whatever it defaults to.
The code is now fully tested, sorry I forgot to tell you to replace the $TaskName variable with a value or to add the variable, as is now shown in the adjusted code.
HTH

How do I use secret variables with inline powershell scripts

So I have some powershell I'm trying to use to setup a Scheduled Task during an ADO deployment. In order to get the task set to "Run whether user is logged on or not" I am required to create it using a User and Password according to these:
Set a Scheduled Task to run when user isn't logged in
Schedule task creation with option ""Run whether user is logged in or not"" using powershell
https://learn.microsoft.com/en-us/powershell/module/scheduledtasks/new-scheduledtaskprincipal?view=win10-ps
And several others.
So with the security rules of the company all passwords from ADO have to be in secret variables. These do not decrypt when called basically from within the scripts, you'll get null values. According to these, you have to pass them in as Environment Variables and/or Parameters to the script:
https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=classic%2Cbatch
https://github.com/Microsoft/azure-pipelines-tasks/issues/8345
VSTS: Pass build/release variables into Powershell script task
https://github.com/microsoft/azure-pipelines-agent/issues/145
https://adamtheautomator.com/azure-devops-variables-complete-guide/
Many of these also only show the yaml side, but I'm using classic so this is of no use and I suspect irrelevant based on the next link which then contradicts them saying you can only use parameters on file base scripts and not inline:
https://github.com/MicrosoftDocs/vsts-docs/issues/1083
I have setup an Environment Variable per the MS link on variable usage as part of the ADO step where by i have a name and a value defined as $(mySecret).
I have tried accessing this through various means described in the links above:
$MYSECRET
$env:MYSECRET
$($MYSECRET)
$($env:MYSECRET)
$(MYSECRET)
$(env:MYSECRET)
(All of the following with both Param and param)
param([string]$mySecret)
param($mySecret)
param($MYSECRET)
param($env:mySecret)
param($env:MYSECRET)
All of these return a "Param is not a recognized function" which according to these, is usually due to param not being the first word in the script, which is not the case for me, I have already checked, double checked, pulled out the text to notepad, notepad++ (both just in case) and compared, and verified it is really the very first word in the script:
PowerShell 2.0 and "The term 'Param' is not recognized as the name of a cmdlet, function, script file, or operable program"
PowerShell parameters - "The term 'param' is not recognized as the name of a cmdlet"
powershell unable to recognize parameter
I've even tried to copy and paste some of the Param solutions suggested above, even from the ADO git, and they all fail for this. I believe because of the git issue 1083 linked above.
None of the suggestions or answers from any of the links I've posted have worked.
One of the other links I came across had a suggestion to create up to three other deployment steps for creating variables, pulling them from the ADO environment, executing direct decryption and assignment. Completely over the top for what I believe should be required here. Another suggestion was to create an extra step to create a temp function to pull the secret and parse it with substring with a couple of different start and end values and to piece those back together as apparently the substring function could see beyond the encryption. Even if that did work, that is ridiculous. As such I haven't tried these last 2 suggestions. If that's really the case I would like someone to point me to the git docs stating as such or there needs to be a bug written up on it.
I'm simply at a loss. I just need to access a secret variable in an inline powershell script for a single task during and ADO deployment, does anyone know how to achieve this. Note the task creation code below does work when I use plain text inputs for the user and password, but that's against policy so not an option.
Here is my script:
param($taskPass)
$taskName = $env:ScheduledTaskName
$taskExists = Get-ScheduledTask | Where-Object {$_.TaskName -like $taskName }
if(!$taskExists) {
$Trigger = New-ScheduledTaskTrigger -Daily -At 3am
$Actions = (New-ScheduledTaskAction -Execute "powershell curl -Method POST -Uri $env:VarURL"),
(New-ScheduledTaskAction -Execute "powershell Invoke-Sqlcmd -ServerInstance $env:Server -Database 'MyDB' -Query 'EXEC NightlyProc'")
#The following was suggested from here http://duffney.io/Create-ScheduledTasks-SecurePassword
$SecurePassword = "$taskPass"
Write-Host "Pass: $SecurePassword"
$Credentials = New-Object System.Management.Automation.PSCredential -ArgumentList $env:ScheduledTaskUser, $SecurePassword
$Password = $Credentials.GetNetworkCredential().Password
$Task = New-ScheduledTask -Action $Actions -Trigger $Trigger
$Task | Register-ScheduledTask -TaskName $taskName -User $env:ScheduledTaskUser -Password $Password
}
Ok so I finally worked this out. I used the YAML viewer to get a comparison of what the classic interface was creating vs what the MS link said it should be. This involved setting the Environment Variable to the value in the $(mySecret) format (no $env: here just the variable name). Then in the script using the $env:MYSECRET formatting. But also without all of the credential/password manipulation from the duffney.io site. Just setting the -Password parameter for the new Task directly to the $env:MYSECRET variable. No need for params. Task created as 'Run even when user not logged on' just as expected. Final code:
$taskName = $env:ScheduledTaskName
$taskExists = Get-ScheduledTask | Where-Object {$_.TaskName -like $taskName }
if(!$taskExists) {
$Trigger = New-ScheduledTaskTrigger -Daily -At 3am
$Actions = (New-ScheduledTaskAction -Execute "powershell curl -Method POST -Uri $env:URL"),
(New-ScheduledTaskAction -Execute "powershell Invoke-Sqlcmd -ServerInstance $env:Server -Database 'MyDB' -Query 'EXEC NightlyCache'")
$Task = New-ScheduledTask -Action $Actions -Trigger $Trigger
$Task | Register-ScheduledTask -TaskName $taskName -User $env:ScheduledTaskUser -Password $env:TASKPASS
}
Making sure to set an Environment Variable to the following values:
NOTE: CAPS on the name are not necessary here, seems to just be a standard. I've successfully deployed with both the CAPS version and 'TaskPass'. I say this because several of the links I posted above make it seem like they are.
Name: TASKPASS
Value $(ScheduledTaskPass)
Also of note that MS does it's best to hide this value from you. Even if you set it to a local script variable and try to output the value, as I was trying to do for confirmation purposes, you'll still only get asterisks. But it's really there, I/they promise.
In classic type,
In order to access secret variable in inline powershell script,
step1:Define a variable and set it secret variable(using lock icon)( for ex Name: PASSWORD value: ********)
step2: Add/set an Environment variable(below inline script available as an option) to remap your secret variable[since you can't access secret variables directly in scripts(ref: MSdocs)] like for ex Name: PASSWORD value: $(PASSWORD)
step3: While using this variable in script access like $env: PASSWORD

How to configure "If the task is already running, then the following rule applies" in Windows Task Scheduler using PowerShell script?

I am trying to achieve the following settings (select "If the task is already running, then the following rule applies") through PowerShell script but unable to get appropriate settings to configure that.
I am using the following code to configure that
$Trigger = New-ScheduledTaskTrigger -At 07:00am -Daily
$Settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hour 1) -Compatibility Win7 -StartWhenAvailable -Priority 7
$User = "SYSTEM"
$Action = New-ScheduledTaskAction -Execute "some script" -Argument "some argument" -WorkingDirectory "working dir"
Register-ScheduledTask -TaskName "Test Task" -Trigger $Trigger -User $User -Action $Action -Settings $Settings -RunLevel Highest –Force
To do the advanced configuration for the triggers
$Task = Get-ScheduledTask -TaskName "Example Task"
$Task.Triggers[0].ExecutionTimeLimit = "PT10M"
$Task | Set-ScheduledTask -User $User
The setting is configured via New-ScheduledTaskSettingsSet and the parameter you're looking for is -MultipleInstances:
-MultipleInstances
Specifies the policy that defines how Task Scheduler handles multiple instances of the task. The acceptable values for this parameter are:
IgnoreNew. The new task instance is ignored. Parallel. The new task instance starts immediately. Queue. The new task instance starts as soon as the current instance completes.
Type: MultipleInstancesEnum
Accepted values: Parallel, Queue, IgnoreNew
Position: Named
Default value: None
However, the documentation lists only 3 values, and the respective enum (at least at the time of this writing also only has the listed 3 values:
Parallel → Run a new instance in parallel
Queue → Queue a new instance
IgnoreNew → Do not start a new instance
If you create a task manually via the GUI and select the setting "Stop the existing instance" the value .Settings.MultipleInstances is empty, but if you create a Settings object via New-ScheduledTaskSettingsSet omitting the parameter -MultipleInstances it defaults to IgnoreNew. Attempts to change that to an empty value result in validation errors.
This is obviously a bug (missing value in the referenced enum).
The enum now contains 'StopExisting'.
This is my solution in C#.
static void RegisterMyTask(string taskPath, string remoteServer)
{
try
{
using TaskService ts = new(remoteServer);
TaskDefinition taskDef = ts.NewTask();
taskDef.Settings.MultipleInstances = TaskInstancesPolicy.StopExisting;
...
However PowerShell probably has the new enum as well. As I would guess:
-MultipleInstances StopExisting
I had a task scheduled on multiple remote servers.
On day 1 it started running.
Day 2 it failed to start a new instance, but didn't kill the existing instance
Day 3 it failed to start a new instance, but didn't kill the existing instance. Then 30 seconds later, the existing instance was killed by the day 1 timeout setting, but the new instance had already failed for the day.
This fixed my problem.

Powershell Scheduled Task At Startup Repeating

I'm trying to create a Scheduled Task with the following Trigger:
- Startup
- Run every 5 minutes
- Run indefinitely
In the GUI I can do this easily by selecting:
- Begin the task: at startup
And in the Advanced tab:
- Repeat task every: 5 minutes
- For a duration of: indefinitely
But I'm having trouble doing it with Powershell.
My troubled code:
$repeat = (New-TimeSpan -Minutes 5)
$duration = ([timeSpan]::maxvalue)
$trigger = New-ScheduledTaskTrigger -AtStartup -RepetitionInterval $repeat -RepetitionDuration $duration
It won't take the RepetitionInterval and RepetitionDuration parameters. But I need that functionality.
How could I accomplish my goal?
To set the task literally with a "Startup" trigger and a repetition, it seems you have to reach in to COM (or use the TaskScheduler UI, obviously..).
# Create the task as normal
$action = New-ScheduledTaskAction -Execute "myApp.exe"
Register-ScheduledTask -Action $action -TaskName "My Task" -Description "Data import Task" -User $username -Password $password
# Now add a special trigger to it with COM API.
# Get the service and task
$ts = New-Object -ComObject Schedule.Service
$ts.Connect()
$task = $ts.GetFolder("\").GetTask("My Task").Definition
# Create the trigger
$TRIGGER_TYPE_STARTUP=8
$startTrigger=$task.Triggers.Create($TRIGGER_TYPE_STARTUP)
$startTrigger.Enabled=$true
$startTrigger.Repetition.Interval="PT10M" # ten minutes
$startTrigger.Repetition.StopAtDurationEnd=$false # on to infinity
$startTrigger.Id="StartupTrigger"
# Re-save the task in place.
$TASK_CREATE_OR_UPDATE=6
$TASK_LOGIN_PASSWORD=1
$ts.GetFolder("\").RegisterTaskDefinition("My Task", $task, $TASK_CREATE_OR_UPDATE, $username, $password, $TASK_LOGIN_PASSWORD)
New-ScheduledTaskTrigger uses parameter sets. When you specify that you want the scheduled task to start up "at logon" you are restricting yourself to the following parameter set:
Parameter Set: AtStartup
New-ScheduledTaskTrigger [-AtStartup] [-RandomDelay <TimeSpan> ] [ <CommonParameters>]
What may be more beneficial is if you use your "at startup" scheduled task to register a new scheduled task to run every five minutes using the "once" parameter set:
Parameter Set: Once
New-ScheduledTaskTrigger [-Once] -At <DateTime> [-RandomDelay <TimeSpan> ] [-RepetitionDuration <TimeSpan> ] [-RepetitionInterval <TimeSpan> ] [ <CommonParameters>]
Your Scheduled Task Trigger should successfully be assigned once you are using the correct parameter set.
See my answer here: https://stackoverflow.com/a/61646465/2943191
It is actually relatively simple, once a working example is written, of course.
Instead of -AtLogOn use -AtStartup.
For what it’s worth, I too ran into this issue as I was particularly perturbed that this functionality was pulled from PowerShell for -AtStartup. However, upon further review, the same functionality can be performed by just be using -Once and configuring it to repeat. In the OP’s case of having this run every five minutes, it probably won’t make a difference if this is run at startup or five minutes thereafter.
I have confirmed this with -Once and then configuring it to repeat. It will also perform the repeats when the system is restarted. This is somewhat different from #BryceMcDonald’s answer in that you don’t need to create two triggers—just create the -Once one and you’ll be good to go.

Powershell: Set a Scheduled Task to run when user isn't logged in

I have been using the Powershell Scheduled Task Cmdlets to create a scheduled task on our servers.
How do I elect to 'Run whether a user is logged in or not using this API?
I've created action, trigger, principal and settings objects, and passed them to Register-ScheduledTask, as below:
$action = New-ScheduledTaskAction -Execute foo.exe -Argument "bar baz"
$trigger = New-ScheduledTaskTrigger -Once -At $startTime -RepetitionInterval (New-TimeSpan -Minutes 1) -RepetitionDuration ([Timespan]::MaxValue)
$principal = New-ScheduledTaskPrincipal -UserId "$($env:USERDOMAIN)\$($env:USERNAME)" -LogonType ServiceAccount
$settings = New-ScheduledTaskSettingsSet -MultipleInstances Parallel
Register-ScheduledTask -TaskName $taskName -TaskPath "\my\path" -Action $action -Trigger $trigger -Settings $settings -Principal $principal
When I create a scheduled task like this, it defaults to 'Run only when the user is logged on.
This question shows how to do so using COM objects, and this one using schtasks.exe, but how do I do it using the *-ScheduledTask* cmdlets?
I do not like or approve of the currently highest rated answer as then you have to know your credentials into a script to do this and can't do this from something like Packer or some other system/configuration automation. There is a better/proper way to do this which Aeyoun mentioned but didn't go into details about which is to properly set the principal to run as the system user.
$action = New-ScheduledTaskAction -Execute foo.exe -Argument "bar baz"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 1) -RepetitionDuration ([Timespan]::MaxValue)
$principal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -MultipleInstances Parallel
Register-ScheduledTask -TaskName "tasknamehere" -TaskPath "\my\path" -Action $action -Trigger $trigger -Settings $settings -Principal $principal
You need to remove $principal and register the task with a user and password:
Register-ScheduledTask -TaskName $taskname `
-TaskPath "\my\path" `
-Action $action `
-Trigger $trigger `
-User "$env:USERDOMAIN\$env:USERNAME" `
-Password 'P#ssw0rd' `
-Settings $settings
The “Run whether user is logged in or not” option in the Task Scheduler GUI is equivalent to New-ScheduledTaskPrincipal -LogonType S4U.
Primer on Creating Scheduled Tasks via PowerShell
I, too, was trying to create a scheduled task on Windows Server 2019 using PowerShell. None of the answers worked. It seems like all the answers really have bits and pieces of the correct solution, but none had the full solution. While some of the answers may have worked for some people, it was really all luck based on their existing system, security settings, and other factors, I'm sure. There's a lot that I learned on my journey to creating a really simple scheduled task via PowerShell to collect some application telemetry.
So I'm going to completely revise my original answer and take you step-by-step through what it takes (in the most ardent of cases) to create a Scheduled Task, especially on a server. If only it were as simple as cron.
Assigning Logon as a batch job for users running unattended tasks
When most people want to create a scheduled task, especially for server/application maintenance, or just to run something on a periodic basis, the first stop is the Windows Task Scheduler. A nice GUI is provided (well, it could be nicer/modernized, but hey, at least it's a GUI) where you can specify all the details necessary to get you going. The problem is, you can't automate the GUI. And as I found out, the GUI is doing things behind the scenes that Microsoft isn't exactly forthright in telling you how it's doing it (or how it's not doing it, for that matter). And that can lead to a lot of issues—including the task just not running or even the user account under which the task runs becoming locked.
In Microsoft's documentation on the Logon as a batch job (SeBatchLogonRight) under the Group Policy heading, Microsoft states that, "Task Scheduler automatically grants this right when a user schedules a task." Well, this statement is not always true.
When you create a scheduled task using the Task Scheduler GUI, yes, if the scheduled task is configured to Run whether the user is logged on or not and the user does not have the Logon as a batch job right, then the Task Scheduler will assign that right to the user (unless that default is changed—see the referenced link above).
However, when creating a scheduled task using the PowerShell ScheduledTask module's cmdlets, this automatic user rights assignment DOES NOT occur. So you must do this manually. The GUI way to do this is to use the Local Security Policy MMC (Microsoft Management Console). Of course, in an automation scenario, the GUI is out, so your friend here will be secedit.exe. (Myself, I wrote PowerShell wrappers around secedit.exe.)
So, let's assume we have a scheduled task that will collect telemetry for an application running on your server, which will then send that telemetry to your telemetry gathering service, such as New Relic or Data Dog, for example. The task will run under the user account CONTOSO\AppTelemetry. Given we'll be creating the scheduled task via PowerShell in an automated fashion during blue/green deployments, we need to assign this user the Logon as a batch job user right.
Using secedit to assign user rights
The steps are quite simple here:
Export the existing server's security policy to a security policy template (optionally, only containing the sections you care about)
Create a new security policy template
Add the security identifier (SID) of the user under which the scheduled task will run to the corresponding user right in the new security policy template
Import the security policy template containing the security policy changes to a new database
Configure the system's security policy to incorporate the changes contained in the security policy database created in step 4.
Let's go through the code that will make this happen.
Export the existing server's security policy to a security policy template
You can do this from CMD or from PowerShell (Desktop edition or Core, it doesn't matter). All examples will be in PowerShell, unless noted otherwise.
secedit.exe /export /cfg secpol.inf /areas USER_RIGHTS
The above command exports the system's security policy, but only the section that contains information on User Rights assignments. If you need to add additional settings, such as registry keys, this can also be done via security policy. Read the Microsoft documentation on secedit.exe or Security Policies, in general.
Create a new security policy template and add our scheduled task user's SID to the SeBatchLogonRight value
Now we need to make a new security policy template which contains the delta of what we want to be the new security policy vs. what is the current security policy. I recommend that the new security policy template contain as little information as possible—only those settings which need to be changed.
One would think that all you need to do is specify that the SeBatchLogonRight must include the SID for the user, and that would be it. But if you thought that, you'd be wrong. The SeBatchLogonRight has some users assigned to it by default (see the above referenced link to the Microsoft docs). If we were to just assign our user's SID to this right in the new policy template, it would effectively replace the existing value in the system's security policy, not update it. So since we're making an additive change, we need to add this user right to our template with the existing value from the system security policy that we exported above, and then add our user SID to the list.
By default, the SeBatchLogonRight has these SIDs assigned to it:
SeBatchLogonRight = *S-1-5-32-544,*S-1-5-32-551,*S-1-5-32-559,*S-1-5-32-568
These are some "well-known" SIDs for some standard Windows security groups whose users should have this right. Let's assume the SID for CONTOSO\AppTelemetry is S-1-5-21-0000000000-1111111111-2222222222-3333. But wait—how do you get that?
function ConvertTo-SecurityIdentifier {
Param (
[Parameter(Position = 0, Mandatory, ValueFromPipeline)]
[string[]] $UsernameOrGroup
)
Process {
foreach ($Name in $UsernameOrGroup) {
$Account = New-Object -Type System.Security.Principal.NTAccount `
-Argument $Name
$Account.Translate([System.Security.Principal.SecurityIdentifier]).Value
}
}
}
Set-Alias -Name ConvertTo-SID -Value ConvertTo-SecurityIdentifier
function ConvertFrom-SecurityIdentifier {
Param (
[Parameter(Position = 0, Mandatory, ValueFromPipeline)]
[string[]] $SecurityIdentifier
)
Process {
foreach ($SID in $SecurityIdentifier) {
$Account = New-Object -Type System.Security.Principal.SecurityIdentifier `
-ArgumentList $SID
$Account.Translate([System.Security.Principal.NTAccount]).Value
}
}
}
Set-Alias -Name ConvertFrom-SID -Value ConvertFrom-SecurityIdentifier
(Why didn't I simply use Get-ADUser to get the SID for a user account? Two reasons. First, it's not as straightforward to get a user account name from a SID using Get-ADUser. It can be done, but the code above is clearer. Secondly, and most importantly, not all users will have Get-ADUser installed on their machines. My previous laptop had the Windows Remote Server Administration Tools (RSAT) installed, and so I had Get-ADUser available. Without RSAT installed or installing the Active Directory PowerShell module, Get-ADUser is not available. The cmdlets above work without a dependency on anything except Windows and the .NET framework, which, if you're trying to get a user account SID and using PowerShell to do so, by definition, you most likely have the two prerequisites.)
Then you can simply run:
$SID = 'CONTOSO\AppTelemtry' | ConvertTo-SecurityIdentifier
Now that we have our SID, we can create a security policy template (note, there are better ways to do this—I created PowerShell cmdlets that let me programmatically interact with these INF files, but I'm just going to use here documents). Note that in the security policy you exported, all SIDs are prefixed with an *, and that multiple values are comma-delimited:
$NewPolicy = #'
[Unicode]
Unicode=yes
[Version]
signature="$CHICAGO$"
Revision=1
[Privilege Rights]
SeBatchLogonRight = *S-1-5-32-544,*S-1-5-32-551,*S-1-5-32-559,*S-1-5-32-568,*S-1-5-21-0000000000-1111111111-2222222222-3333
'#
$NewPolicy | Set-Content batchlogon.inf
Import the new security policy template to a new security policy database
The heading says it all:
secedit.exe /import /db batchlogon.sdb /cfg batchlogon.inf
Configure the system security policy with the changes contained in the new security policy database
Once again, the heading says it all:
secedit.exe /configure /db batchlogon.sdb
And that's all there is to assigning a user the Logon as a batch job right from the CLI.
Create a Scheduled Task via PowerShell
Now that the user has the right to run the scheduled task whether or not they are logged in, we need to create a scheduled task that will run whether or not the user is logged in. You will see many links to many Q&A answers that say you need this or that LogonType (notably, S4U or ServiceAccount) or run it with the highest privileges, etc. Don't listen to any of that. It's (mostly) wrong. This section will outline the minimum necessary steps to achieve creating a scheduled task that runs whether the user is logged on or not.
Create the Scheduled Task Action
First up is creating the task action. Scheduled tasks are made up of a few parts:
Actions
Triggers
Settings
Principal(s)
Registration
The names are fairly self-explanatory. We'll be creating a scheduled task action which executes a program. (There are other action types. See the documentation on Task Actions.)
The important thing to know about Exec actions is that the Task Scheduler service will spawn an instance of cmd.exe to run the provided program. This means that if you need to specify arguments to the program, you need to ensure you follow the difficult quoting rules for CMD commands. (Depending on your arguments, these quoting rules can require a lot of testing to ensure the command will run as expected. In even my simple case, I got it wrong, and the task seemed to run correctly—a 0 exit code—but did nothing at all!) See cmd.exe /? for all the gory details. Also, you can find a lot of info through a web search.
Let's create the task action:
$TaskAction = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument ('-NoLogo -ExecutionPolicy Bypass -NoProfile -NonInteractive' + `
'-File "C:\Telemetry\Send-ApplicationTelemetry.ps1"')
Again, be careful of quoting rules (i.e. if your file path has spaces in it, or, in the case you pass a bit of powershell script to the -Command parameter as opposed to using -File as I did here). There are a few things to point out here:
I could've used pwsh.exe instead to run PowerShell Core
-NoLogo avoids printing the "banner" that appears when you start PowerShell. It can make redirected output to log files nicer if you do such a thing.
-ExecutionPolicy Bypass specifies that this script will bypass any current execution policy on the system. The default execution policy is Restricted, which disallows any script from running unless a full path is specified, and disallows any scripts which came from remote sources. This switch basically ensures that the script will be run. It may not always be necessary, but if you trust the script you're scheduling, this shouldn't hurt either.
-NoProfile is used to not load the user's PowerShell profile (it's a similar concept to a user's bash profile on Linux OSes). However, there can also be global profile scripts. Unless you know you need profile scripts to run when PowerShell starts, it doesn't hurt to add this switch, and most likely will prevent errors.
-NonInteractive is a very important switch. Basically, this prevents powershell from prompting for user input (e.g. for cmdlets which require confirmation or user imput). This also means if your script does require confirmation/user input, it won't work non-interactively (i.e. when the user is not logged on).
-File tells PowerShell to execute the specified file. You could also use -Command instead and pass in some "stringified" PowerShell code.
Create the Scheduled Task Principal
This is one of the most important steps for properly creating a scheduled task that will run whether the user is logged in or not. In fact, this step is REQUIRED to configure a scheduled task to Run whether the user is logged on or not.
$TaskPrincipal = New-ScheduledTaskPrincipal -Id 'Author' `
-UserId 'CONTOSO\AppTelemetry' `
-LogonType Password `
-RunLevel Limited
Let's go over these settings in a bit more detail.
-Id
This is a free-form text field, so much as I can tell. The Task Scheduler GUI always uses the term 'Author', but generally, you can use whatever you want here. The caveat to that is a scheduled task can contain many different actions (up to 32). And Actions also have a "context" associated with them. The task scheduler automatically assigns the context to 'Author' (for simple, single-action tasks). The documentation seems to indicate that you can provide more than one principal to a scheduled task, and the Id of the principal, in correlation with an action's context, will determine under which principal an action will execute. For our single-action task, we'll just go with 'Author'.
-UserId or -GroupId
A task can run under a user account or any user belonging to a specified Group. Be aware that when using a group, only when a user of the group is logged on will the scheduled task run. That's because using a group ID, you cannot specify a password for the task.
In 99% of the cases, you're going to want to use -UserId as I did above. It doesn't need to be a fully qualified user Id. Note that we don't store the password with the principal. That comes later.
-LogonType
This is probably one of the most misunderstood parameters to this cmdlet. The documentation on LogonType is pretty good. Too bad the PowerShell help for New-ScheduledTaskPrincipal doesn't link to it (I put in feedback about that).
Basically, you'll probably never need to be concerned with the S4U logon type. It may be relevant in some scenarios (which is why it exists), but for most everything you'll want to do, it probably won't be.
You'll only use the ServiceAccount logon type if the scheduled task will run under the NT AUTHORITY\LOCALSYSTEM, NT AUTHORITY\SYSTEM user accounts, for example. (There's a third one, but the name escapes me at the moment.) Since our task is not running under a system account, this is not the value we want.
As you might be able to guess, Interactive means the task will be run under the context of a logged on user. This can be useful when the task needs to start a GUI application, or the task action is Message Box to display a dialog on the system. Obviously, this is not useful in our case. There's also the InteractiveOrPassword type which combines this logon type, with the one we want, Password. And so I won't discuss it further.
And now, of course, the Password logon type, which is the one we want. This says that a password will be stored with the scheduled task, which will be used to logon the user (as a batch job) so the task can run whether the user is logged on or not. Yes, this is the value which checks the Run whether the user is logged on or not checkbox in the Task Scheduler UI.
-RunLevel
Think of this option as specifying whether or not the task needs to run with elevated privileges. When you run a command, such as installing some software, the User Access Control (UAC) kicks in and displays a dialog asking if you want to allow the program to make changes to your computer. You probably click Yes half the time without even reading it. Setting this parameter to Highest is like clicking Yes to the UAC dialog (or just starting the process with elevated privileges, i.e. Run as Administrator). This is a bad thing, unless you know you need it. You should always start with Limited, and if you find the scheduled task won't run under those privileges, then elevate. So that's what I've done. I'm starting with the principle of least privilege and using Limited run level.
Creating the Scheduled Task trigger
You can have one or more triggers to trigger a schedled task. There are different trigger types. See the documentation for these different types. I'm just going to do a very simple one:
$TaskTrigger = New-ScheduledTaskTrigger -Once -At ([DateTime]::Now.AddMinutes(5)) `
-RepetitionInterval ( `
[TimeSpan]::FromMinutes(5) `
)
I just want a task that will run once, starting 5 minutes from now, and then run subsequently every 5 minutes after that.
Task Settings Set
If I needed to customize some other task settings, I could do so by using New-ScheduledTaskSettingsSet. I'm fine with the defaults, so I'll skip this. But the Task Scheduler documentation details it all, and really, the settings are quite self-explanatory for the most part (except the RunOnlyIfNetworkIsAvailable, but it's not overly difficult to understand).
Create the Scheduled Task
Now you might think this is where we create the task and we'll get to see it in Task Scheduler. Wrong. But we will be creating an Scheduled Task object. You MUST do this step, or your task will not be registered correctly—most importantly, it will not be setup to Run whether the user is logged in or not.
$Task = New-ScheduledTask -Description 'Collects application telemetry' `
-Action $TaskAction `
-Principal $TaskPrincipal `
-Trigger $TaskTrigger
All that's happening here is we're creating a scheduled task instance. But it has not been added to the list of tasks in the task scheduler. That's known as registration, and we'll be doing that next. If you just type $Task in your shell after executing the above command, you'll notice that the task name and path will be blank. You can't even specify it as part of calling New-ScheduledTask. Again, that, oddly enough, happens during registration. So let's talk about the most important part, registration.
Registering the Scheduled Task
It is true that I could simply:
$Task = Register-ScheduledTask -TaskName 'Foo' -TaskPath `\` `
-Action $TaskAction -Trigger $TaskTrigger `
-User 'SomeUser' -Password '$uper53cr37'
But this would result in a task that runs interactively (Run only when the user is logged on). And this is not what we want. Which is why we created the task principal above, and a task instance containing the task principal.
So let's register our task:
$Task = $Task | Register-ScheduledTask -TaskName 'Collect Telemetry' `
-TaskPath '\Awesome App' `
-User 'CONTOSO\AppTelemetry' `
-Password 'ShhD0ntT3ll4ny0n3!'
So as you can see, yes, you MUST supply a password. There are various ways to do this safely and securely in an automated fashion—but alas, not so easy for demonstration purposes. The important part here is the user and password provided to Register-ScheduledTask. The specified user MUST be the same user as specified in the principal we created. And obivously, the password for that user must be correct. IF you use a different user to register the scheduled task, the task prinipal WILL be updated to run the task using the user account specified when registering the task. (That's documented here.)
And that's all there is to scheduling a task to run whether the user is logged on or not using PowerShell. But there's one more topic to discuss before I finish: updating a scheduled task.
Updating a Scheduled Task
Believe it or not, this is not as straight forward as it shoud be. The syntax for Set-ScheduledTask is not documented correctly. I forgot to set the Working Directory for my task action above. (Let's assume I need to write a file somewhere but the code that writes the file doesn't use an absolute path.) Let's fix the action and update the task.
This is the way to do it properly. There are different ways you can use Set-ScheduledTask, but I found that other ways of doing it can lock the user account or just plain not work (i.e. give you an error). It's not to say those other ways are wrong, but just for this particular kind of change, this is what worked for me every time:
$Task = Get-ScheduledTask -TaskName 'Collect Telemetry' -TaskPath '\Awesome App'
$Task.Actions[0].WorkingDirectory = 'C:\AwesomeApp'
$Task | Set-ScheduledTask -User 'CONTOSO\AppTelemetry' -Password 'ShhD0ntT3ll4ny0n3!'
Again, it's important to use the same user and password used when registering the task. If you use a different username/password, then the task principal WILL ALSO be updated. That's probably not what you want.
That's pretty much all there is to working with Scheduled Tasks in Powershell.
Use schtasks for debugging help
One way to help yourself when you're having trouble creating scheduled tasks via PowerShell is to create a similar scheduled task on your local computer via the GUI. Then you can use the schtasks command to query for the scheduled task you created via the GUI and compare it with the scheduled task you created via PowerShell. To use schtasks, for example:
# To get the XML for the task:
schtasks /query /tn '\My Task Path\MyTask' /xml ONE
# To get a nice formatted list of the task properties:
schtasks /query /tn '\My Task Path\MyTask' /v /fo LIST
You can also get a scheduled task's XML representation via PowerShell using Export-ScheduledTask. You can find documentation for all of the PowerShell Scheduled Task cmdlets here. The documentation is OK. Some of it is misleading, most of it is incomplete (i.e. it assumes you have knowledge about the Task Scheduler apart from the PowerShell cmdlets themselves). Documentation on the Task Scheduler, it's COM interfaces, XML schema, etc., can be found here.
Conclusion
I hope this helps someone, because it took me a long time to figure this all out. Mainly, a lot of, "Why isn't this task doing anything even when it says its successful?" Or "Why does the account keep getting locked out?" (Wrong logon type, password, or user rights assignment, or all 3!)
Once you have the task set up in the gui, run this
$task = Get-ScheduledTask "test task for notepad"
$task.Principal.LogonType = "Password"
Set-ScheduledTask $task
also control Run level check:
RunLevel
Specifies the required privilege level to run tasks that are
associated with the principal.
e.g.: "Highest" or "Limited"
I had a similar challenge when trying to create a scheduled task on Powershell to copy files to a mapped drive.
Here's how I solved it:
First, I had to use the UNC path to specify the path of the mapped drive:
Get-ChildItem -Path "C:\MyFiles\*" -Include *.jpg -Recurse | Copy-Item -Destination "\\192.168.54.20\CopiedFiles"
Next, I set up the scheduled job with the commands below:
$TaskName = "FileSync"
$Description = "This task will run periodically to sync .fin files from a specified source directory to a specified destination directory"
$ScriptPath = "C:\Users\my_userDesktop\file_sync.ps1"
$UserAccount = "COMP1\my_user"
$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-ExecutionPolicy Bypass -File $ScriptPath"
$Principal = New-ScheduledTaskPrincipal -UserID $UserAccount -LogonType ServiceAccount
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 1) -RepetitionDuration ([System.TimeSpan]::MaxValue)
Register-ScheduledTask -TaskName $TaskName -Action $Action -Description $Description -Trigger $Trigger -Principal $Principal
Note: The $Principal to allow the task to run whether the user is logged in or not is very essential for a scheduled job that is to sync to a mapped drive.
That's all.
I hope this helps