The documentation is not very clear about it, but as it turns out, both -Filepath
and -Scriptblock
options will execute script blocks. And therefore, the automatic variable $PSScriptRoot
won't be properly initialized, since this only happens when the caller is a script. In both cases, the caller is the TaskScheduler which will call Powershell.exe
with some extra arguments, including the -Command
which in turn, will load a job definition from serialized XML, that will ultimately load the contents of the given file.
The way it works under the hood is, when using the -Filepath
option of the Register-ScheduledJob
, the cmdlet creates a somewhat special ScheduledTask that will execute Powershell with the following arguments:
-NoLogo -NonInteractive -WindowStyle Hidden
-Command "Import-Module PSScheduledJob; $jobDef = [Microsoft.PowerShell.ScheduledJob.ScheduledJobDefinition]::LoadFromStore('MyScheduledJob', '%LOCALAPPDATA%\Microsoft\Windows\PowerShell\ScheduledJobs'); $jobDef.Run()"
As you can see, there is an ad-hoc command, which loads a specialized module that does the heavylifting for reading/loading and storing "job definitions" in an XML store. If you navigate to that folder, there will be one folder per ScheduledJob, with a ScheduledJobDefinition.xml
which contains an XML tag called <InvocationParam_FilePath>
whose value is still only the path to the file that contains the script to be executed. However, when the task is triggered, it will go and extract the contents of that file and run it as a ScriptBlock. This allows you to update the script file and the Task Scheduler will always pick up the most updated version when it runs (that's great!), while you can still trace what was exactly run each time because under the Output
directory you can find a Results.xml
file that contains a snapshot of the code that was actually executed (<Status_Command>
).
So, as you already discovered, one simple workaround is to make it so that the script block is only a wrapper to an invocation of a script file, like in your example:
& "C:\Scriptpath\Script.ps1"
# This will effectively auto-populate
# the $PSScriptRoot variable within the
# boundaries of that script, making it
# possible to be used within it.
The second workaround, if you really would like to use -Filepath
option, is to use it in tandem with the -ArgumentList
option to inject those values when the block gets executed. Because I register my scheduled jobs with another powershell script placed in the same location, which I interactively execute, all those auto-variables will be available at registration-time, and the values will be stored in the job definition (<InvocationParam_ArgList>
) as part of its "execution context" so to speak. Now, you just need to have a way to make the script work both when called interactively and when called by the Task Scheduler. Here's an example of a backup script that I needed it to work in such way. This is how the ScheduledJob is registered:
Register-ScheduledJob -Name $taskName `
-FilePath $pathToBackupScript `
-ArgumentList $PSScriptRoot,$SpecialFolderDropbox `
-RunEvery (New-TimeSpan -Days 1)
And within the script, I ensure those parameters will always have correct values (with default values, if not provided via an ArgumentList):
# BackupScript.ps1
param (
[String] $ScriptRoot=$PSScriptRoot,
[String] $Dropbox=$SpecialFolderDropbox
)
#...Here goes code that can read from
# those passed parameters
# When run interactively, they will already
# have auto-populated values.
# If you want to keep using $PSScriptRoot instead
# of a proxy variable, you can but some linters
# will complain/warn about this overwrite.
# Example: $PSScriptRoot=$PSScriptRoot...
# Just let it know who's boss :P
The third workaround is to simply go back and use good old Scheduled Tasks (in Windows). Here you can define the command to be run as you wish. Obviously, you will be invoking Powershell.exe ...<the usual switches>... -File script.ps1
, and the script will properly populate the $PSScriptRoot
. Personally, I haven't found any good scenario where I can assert that Scheduled Jobs are the definitive superior choice.