Skip to content

Instantly share code, notes, and snippets.

@Bill-Stewart
Last active March 31, 2025 14:56
Show Gist options
  • Save Bill-Stewart/ce137f690775720036ac63664aee2ba3 to your computer and use it in GitHub Desktop.
Save Bill-Stewart/ce137f690775720036ac63664aee2ba3 to your computer and use it in GitHub Desktop.
# Start-ScheduledTaskCommand.ps1
# Written by Bill Stewart (bstewart AT iname.com)
# This script provides a simple way to execute a command one one or more remote
# computers using the Task Scheduler service. Uses the Task Scheduler scripting
# objects:
# https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-objects
# If you're using the Windows firewall, you'll need the 'Remove Scheduled Tasks
# Management' inbound rules (or equivalent) in place on remote machines.
#requires -version 3
<#
.SYNOPSIS
Schedules a command to run once using the Task Scheduler.
.DESCRIPTION
Schedules a command to run once using the Task Scheduler.
.PARAMETER TaskCredential
Specifies a PSCredential object that contains the credentials to run the task.
.PARAMETER TaskServiceAccountName
Specifies the name of a service account to run the task. This can be a SID if you specify a '*' as the first character (e.g., '*S-1-5-19' specifies the 'LOCAL SERVICE' account). You can specify a Managed Service Account (MSA) name by appending '$' to the account name (e.g. 'AppService$').
.PARAMETER Command
Specifies the command to run. Note that the command will run without a visible user interface, so it should run to completion without user intervention (e.g., a quiet application installation).
.PARAMETER Parameters
Specifies the command's command-line parameters. Specify an empty string ('') if the command doesn't need any parameters. (NOTE: It is strongly recommended not to specify any sensitive information, such as a password, in this parameter.)
.PARAMETER ComputerName
Specifies the names of one or more computers on which to start the task. You can specify a dot ('.') or the name 'localhost' to schedule the task to run on the current computer.
.PARAMETER TaskName
Specifies the name of the scheduled task. If you omit this parameter, the task name will automatically be generated based on the command name and the current date and time. If the task already exists, it will not be overwritten unless you also specify -Force.
.PARAMETER Description
Specifies a description for the scheduled task.
.PARAMETER StartTime
Specifies when the task should start. If not specified, or if the specified start time has already passed, the task will be scheduled for 2 minutes from the current date and time.
.PARAMETER ConnectionCredential
Specifies credentials to connect to the computer(s). If not specified, the current credentials are used to connect.
.PARAMETER RunElevated
Specifies to run the task using highest privileges. If not specified, the task will run with limited privileges.
.PARAMETER DeleteTask
Configures the scheduled task to be automatically deleted one minute after its trigger expires. If not specified, the scheduled task will not be automaticaally deleted.
.PARAMETER Force
If you specify -TaskName, this parameter forces overwriting of the task if it already exists.
.INPUTS
System.String
.OUTPUTS
PSObjects with the following properties:
ComputerName Computer on which task is scheduled
AccountName Account used to run the scheduled task
TaskName Scheduled task name
Command Command to run
Parameters Parameters for the command
Elevated Whether the task runs with highest privileges
StartTime Start time for scheduled task
DeleteAfter If -DeleteTask specified, when task can be deleted
ErrorCode Task Scheduler result: 0 for success, or hexadecimal error code
Status Result of Task Scheduler request
#>
[CmdletBinding(DefaultParameterSetName = "ServiceAccount")]
param(
[Parameter(ParameterSetName = "Credential",Position = 0,Mandatory)]
[ValidateNotNullOrEmpty()]
[Management.Automation.PSCredential]
$TaskCredential,
[Parameter(ParameterSetName = "ServiceAccount",Position = 0,Mandatory)]
[ValidateNotNullOrEmpty()]
[String]
$TaskServiceAccountName,
[Parameter(ParameterSetName = "Credential",Position = 1,Mandatory)]
[Parameter(ParameterSetName = "ServiceAccount",Position = 1,Mandatory)]
[ValidateNotNullOrEmpty()]
[String]
$Command,
[Parameter(ParameterSetName = "Credential",Position = 2)]
[Parameter(ParameterSetName = "ServiceAccount",Position = 2)]
[String]
$Parameters,
[Parameter(ParameterSetName = "Credential",Position = 3,Mandatory,ValueFromPipeline)]
[Parameter(ParameterSetName = "ServiceAccount",Position = 3,Mandatory,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[String[]]
$ComputerName,
[Parameter(ParameterSetName = "Credential")]
[Parameter(ParameterSetName = "ServiceAccount")]
[String]
$TaskName,
[Parameter(ParameterSetName = "Credential")]
[Parameter(ParameterSetName = "ServiceAccount")]
[String]
$Description,
[Parameter(ParameterSetName = "Credential")]
[Parameter(ParameterSetName = "ServiceAccount")]
[DateTime]
$StartTime,
[Parameter(ParameterSetName = "Credential")]
[Parameter(ParameterSetName = "ServiceAccount")]
[Management.Automation.PSCredential]
$ConnectionCredential,
[Parameter(ParameterSetName = "Credential")]
[Parameter(ParameterSetName = "ServiceAccount")]
[Switch]
$RunElevated,
[Parameter(ParameterSetName = "Credential")]
[Parameter(ParameterSetName = "ServiceAccount")]
[Switch]
$DeleteTask,
[Parameter(ParameterSetName = "Credential")]
[Parameter(ParameterSetName = "ServiceAccount")]
[Switch]
$Force
)
begin {
# Constants
$SCHED_S_TASK_READY = 0x00041300
$SCHED_E_ACCOUNT_NAME_NOT_FOUND = 0x80041310
$SCHED_E_TASK_NOT_V1_COMPAT = 0x80041327
$TASK_ACTION_EXEC = 0
$TASK_CREATE = 2
$TASK_CREATE_OR_UPDATE = 6
$TASK_LOGON_PASSWORD = 1
$TASK_LOGON_SERVICE_ACCOUNT = 5
$TASK_RUNLEVEL_LUA = 0
$TASK_RUNLEVEL_HIGHEST = 1
$TASK_TRIGGER_TIME = 1
if ( [Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT ) {
throw "Windows platform required"
}
# Abort if we can't instantiate the COM object
try {
$TaskService = New-Object -ComObject "Schedule.Service"
}
catch {
Write-Error -Exception $_.Exception
exit $_.Exception.HResult
}
# "Immediate if" - if $testExpr is $true, run $trueExpr; else run $falseExpr
function iif {
param(
[ScriptBlock]
$testExpr,
[ScriptBlock]
$trueExpr,
[ScriptBlock]
$falseExpr
)
if ( & $testExpr ) { & $trueExpr } else { & $falseExpr }
}
# Attempts to convert an account name to a
# System.Security.Principal.NTAccount object; if the first character of the
# name is '*', attempt to evaluate as a SID; if identity translation fails,
# abort script
function ConvertTo-IdentityReference {
param(
[String]
$accountName
)
$sid = New-Object Security.Principal.SecurityIdentifier `
([Security.Principal.WellKnownSidType]::NullSid,$null)
if ( ($accountName[0] -ne '*') ) {
try {
$sid = ([Security.Principal.NTAccount] `
$accountName).Translate([Security.Principal.SecurityIdentifier])
}
catch {
}
}
elseif ( $accountName.Length -gt 1 ) {
try {
$sid = [Security.Principal.SecurityIdentifier] `
$accountName.Substring(1)
}
catch {
}
}
if ( $sid.IsWellKnown([Security.Principal.WellKnownSidType]::NullSid) ) {
$exception = New-Object Security.Principal.IdentityNotMappedException `
("{0} - '$accountName'" -f ([ComponentModel.Win32Exception] `
$SCHED_E_ACCOUNT_NAME_NOT_FOUND).Message)
Write-Error -Exception $exception
exit $SCHED_E_ACCOUNT_NAME_NOT_FOUND
}
$sid.Translate([Security.Principal.NTAccount])
}
# Based on an account name or PSCredential object, outputs an object with
# the following properties:
# * FullAccountName: full account name (e.g., authority\accountname)
# * AuthorityName: authority name without account name
# * AccountName: account name without authority name
# * SID: SecurityIdentifier object representing account
# * Password:
# * PSCredential object's Password property (if -credential parameter)
# * $null (if -accountName parameter)
# (Requires ConvertTo-IdentityReference function)
function Get-IdentityInfo {
[CmdletBinding(DefaultParameterSetName = "AccountName")]
param(
[Parameter(ParameterSetName = "AccountName",Position = 0,Mandatory)]
[ValidateNotNullOrEmpty()]
[String]
$accountName,
[Parameter(ParameterSetName = "Credential",Position = 0,Mandatory)]
[Management.Automation.PSCredential]
$credential
)
if ( $PSCmdlet.ParameterSetName -eq "AccountName" ) {
$identity = ConvertTo-IdentityReference $accountName
$password = $null
}
else {
$identity = ConvertTo-IdentityReference $credential.UserName
$password = $credential.Password
}
# Split the account name if it contains '\' character
$p = $identity.Value.IndexOf('\')
if ( $p -ne -1 ) {
$authorityName = $identity.Value.Substring(0,$p)
$accountName = $identity.Value.Substring($p + 1)
}
else {
$accountName = $identity.Value
}
[PSCustomObject] @{
"FullAccountName" = $identity.Value
"AuthorityName" = $authorityName
"AccountName" = $accountName
"SID" = $identity.Translate([Security.Principal.SecurityIdentifier])
"Password" = $password
}
}
# For local system, local service, or network service SIDs, outputs
# $TASK_LOGON_SERVICE account; otherwise, outputs $TASK_LOGON_PASSWORD
# (Note: MSAs use $TASK_LOGON_PASSWORD even though they're service accounts)
function Get-TaskSchedulerLogonType {
param(
[Security.Principal.SecurityIdentifier]
$sid
)
$serviceAccountLogon = `
$sid.IsWellKnown([Security.Principal.WellKnownSidType]::LocalSystemSid) -or
$sid.IsWellKnown([Security.Principal.WellKnownSidType]::LocalServiceSid) -or
$sid.IsWellKnown([Security.Principal.WellKnownSidType]::NetworkServiceSid)
iif { $serviceAccountLogon } { $TASK_LOGON_SERVICE_ACCOUNT } `
{ $TASK_LOGON_PASSWORD }
}
# Outputs a SecureString object as plain-text
function ConvertTo-String {
param(
[Security.SecureString]
$secureStr
)
try {
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureStr)
[Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
}
finally {
if ( $bstr -ne [IntPtr]::Zero ) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
}
# Outputs a DateTime object as a Task Scheduler formatted string
function Get-TimeString {
param(
[DateTime]
$time
)
"{0:yyyy-MM-dd}T{0:HH:mm}" -f $time
}
$BoundParameters = $PSBoundParameters
$TaskIdentityInfo = iif { $PSCmdlet.ParameterSetName -eq "Credential" } `
{ Get-IdentityInfo $TaskCredential } `
{ Get-IdentityInfo $TaskServiceAccountName }
$TaskLogonType = Get-TaskSchedulerLogonType $TaskIdentityInfo.SID
$TaskRunLevel = iif { $RunElevated } { $TASK_RUNLEVEL_HIGHEST } `
{ $TASK_RUNLEVEL_LUA }
$TaskFlags = iif { $Force } { $TASK_CREATE_OR_UPDATE } { $TASK_CREATE }
$StartTime = iif { $BoundParameters.ContainsKey("StartTime") } `
{ [DateTime] (Get-TimeString $StartTime) } `
{ [DateTime] (Get-TimeString (Get-Date).AddMinutes(2)) }
$ConnectIdentityInfo = iif { $BoundParameters.ContainsKey("ConnectionCredential") } `
{ Get-IdentityInfo $ConnectionCredential } { $null }
# Uses the following globals (all start with UPPERCASE letter):
# * TaskService (connects to the Task Scheduler service)
# * Script parameters: Command, Parameters, RunElevated, TaskName, StartTime,
# and DeleteTask
# * ConnectIdentityInfo and TaskIdentityInfo objects
# * Description, TaskLogonType, TaskRunLevel, and TaskFlags
function Start-ScheduledTaskCommand {
param(
[String]
$computerName
)
if ( ($computerName -eq ".") -or ($computerName -eq "localhost") ) {
$computerName = [Net.Dns]::GetHostName()
}
# Construct output object
$outputObject = [PSCustomObject] @{
"ComputerName" = $computerName
"AccountName" = [NullString]::Value
"TaskName" = [NullString]::Value
"Command" = [NullString]::Value
"Parameters" = [NullString]::Value
"Elevated" = [NullString]::Value
"StartTime" = $null
"DeleteAfter" = $null
"ErrorCode" = "0"
"Status" = [NullString]::Value
}
# Try to connect to the Task Scheduler service on the computer
try {
if ( $null -eq $ConnectIdentityInfo ) {
$TaskService.Connect($computerName)
}
else {
$TaskService.Connect($computerName,$ConnectIdentityInfo.AccountName,
$ConnectIdentityInfo.AuthorityName,
(ConvertTo-String $ConnectIdentityInfo.Password))
}
}
catch {
# Failed to connect; update output object and return it
$outputObject.ErrorCode = "0x{0:X8}" -f $_.Exception.HResult
$outputObject.Status = $_.Exception.Message
return $outputObject
}
# Sanity check Task Scheduler service version
if ( $TaskService.HighestVersion -lt 0x00010002 ) {
$outputObject.ErrorCode = "0x{0:X8}" -f $SCHED_E_TASK_NOT_V1_COMPAT
$outputObject.Status = ([ComponentModel.Win32Exception] `
$SCHED_E_TASK_NOT_V1_COMPAT).Message
return $outputObject
}
$name = iif { $TaskName } { $TaskName } `
{ "{0} [{1:yyyyMMdd_HHmmss}]" -f (Split-Path $Command -Leaf),(Get-Date) }
# Create an empty TaskDefinition object to populate
$taskDefinition = $TaskService.NewTask(0)
$taskDefinition.RegistrationInfo.Description = $Description
# Create an ExecAction object and specify command and parameters
$execAction = $taskDefinition.Actions.Create($TASK_ACTION_EXEC)
$execAction.Path = $Command
$execAction.Arguments = $Parameters
# If requested start time has already passed, schedule 2 minutes ahead
$startBoundary = iif { (Get-Date) -ge $StartTime } `
{ (Get-Date).AddMinutes(2) } { $StartTime }
# Create a TimeTrigger object and specify when task should start
$timeTrigger = $taskDefinition.Triggers.Create($TASK_TRIGGER_TIME)
$timeTrigger.StartBoundary = Get-TimeString $startBoundary
# If -DeleteTask specified, add a trigger expiration
if ( $DeleteTask ) {
$endBoundary = $startBoundary.AddMinutes(1)
$timeTrigger.EndBoundary = Get-TimeString $endBoundary
$taskDefinition.Settings.DeleteExpiredTaskAfter = "PT0S"
}
else {
$endBoundary = $null
}
# Configure the Principal object
$principal = $taskDefinition.Principal
$principal.UserId = $TaskIdentityInfo.FullAccountName
$principal.LogonType = $TaskLogonType
$principal.RunLevel = $TaskRunLevel
$taskPassword = iif { $null -eq $TaskIdentityInfo.Password } { $null } `
{ ConvertTo-String $TaskIdentityInfo.Password }
# Register the task definition
try {
$taskFolder = $TaskService.GetFolder("\")
# Void cast discards RegisteredTask output object
[Void] $TaskFolder.RegisterTaskDefinition($name,$taskDefinition,
$TaskFlags,$TaskIdentityInfo.FullAccountName,$taskPassword,
$TaskLogonType)
$outputObject.Status = ([ComponentModel.Win32Exception] `
$SCHED_S_TASK_READY).Message
}
catch {
$outputObject.ErrorCode = "0x{0:X8}" -f $_.Exception.HResult
$outputObject.Status = $_.Exception.Message
}
# Update remainder of output object and output it
$outputObject.AccountName = $TaskIdentityInfo.FullAccountName
$outputObject.TaskName = $name
$outputObject.Command = $execAction.Path
$outputObject.Parameters = $execAction.Arguments
$outputObject.Elevated = $RunElevated.IsPresent
$outputObject.StartTime = $startBoundary
$outputObject.DeleteAfter = $endBoundary
$outputObject
}
}
process {
foreach ( $ComputerNameItem in $ComputerName ) {
Start-ScheduledTaskCommand $ComputerNameItem
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment