Getting Started - Jobs

PowerShell Background Jobs

Jobs in PowerShell allow you to run commands in the background. You use a command to start the job, and then various other commands to check in and receive information from it. Jobs can be started as 64-bit or 32-bit, regardless of what you launch your session as. You can also start jobs that run as a different user.  

Why Use PowerShell Jobs?

PowerShell jobs are handy for a lot of different uses. One could be running a script that contains multiple jobs that will perform different Active Directory tasks. You can run those in parallel, and use a While loop to check in on the jobs as they execute.

In this post you'll see how to start jobs, see running jobs, and get information as jobs complete. 

Creating Jobs

The command to start a job in PowerShell is Start-Job.

Let's use this command to start a job that will wait for one minute, and then execute the command Get-Process.

Start-Job -Name SleepProcess -ScriptBlock {Start-Sleep -Seconds 60; Get-Process}

The output of the job started is displayed after we execute the Start-Job command.

If you wanted to wait for this job to complete (which would make the console unusable until the job completes), you can use the Wait-Job command. With Wait-Job you'll need to specify the Job Name, Id, or simply pipe the Start-Job command to Wait-Job.

Start-Job -Name Wait -ScriptBlock {Start-Sleep -Seconds 60} | Wait-Job

And now we wait for the console to free up after 60 seconds have passed.

Monitoring Jobs

To keep an eye on the running jobs in PowerShell, we'll use the command Get-Job

Get-Job

Get-Job will show us all running jobs by default. We can also narrow it down to just our job via the Job Name or Job Id. Let's try getting this Job via the Job Id.

Get-Job -Id 1

If there were more Jobs running, we'd still only see this Job as we specified that we only want to see the Job with the Id value of 1.

Collecting Job Information

There are few different ways to collect information from running jobs in PowerShell. The official command to do it is Receive-Job. You'll need to specify the Job Name or Id when you use this command. Let's receive the information from this job, and specify the -Keep parameter so we can receive the information again later.

Receive-Job -Id 1 -Keep

You can see it displayed the output of Get-Process. Let's store the job in an object, and look at another way to receive the information.

This is a bit interesting. PowerShell stores most of the job information in a child job for each job you run. To access this information, you must access the ChildJobs property for the first child job, or 0. Let's take a look.

$ourOutput = Start-Job -Name GetOutput -ScriptBlock {Get-Process}

Here we started a job, and stored the object in the $ourOutput variable.

Now we can use that variable to take a look at the results.

$ourOutput

Let's see the available methods and properties for $ourOutput.

$ourOutput | Get-Member

Hey, there's the ChildJobs property! Let's take a look at it's properties and methods as well.

$ourOutput.ChildJobs[0] | Get-Member

Both $ourOutput and $ourOutput.ChildJobs[0] have the Output property. However, since our job is actually spawned as child job, this property will be empty on the initial object, and have a value only on the child object. 

$ourOutput.Output
$ourOutput.ChildJobs[0].Output

Error Handling

Error handling in jobs can be tricky, as sometimes a job state will show as completed, but the output of the job is actually a failed command. There are other times where a job will state that is actually has failed. In each case, there are two main spots to check for error information when a job fails.

Let's take a look at the two examples. I can replicate different states with the New-Item command and toggling the -ErrorAction parameter to Stop.

Job State Failed

$failJob = Start-Job -Name FailJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory -ErrorAction Stop}

Let's look at the $failJob object to make sure it has indeed failed...

$failJob

Alright... now here is the weird part. Normally you'd look at the ChildJob's Error property to get the error message. That... or Receive-Job would show you an error. I have tried all of those approaches. It looks like Receive-Job will display the error, but you're unable to store it in a variable or parse the message in any way. Not helpful! 

$jobError = Receive-Job $failJob
$failJob.ChildJobs[0].Error
$jobError

So how do we see what happened? We need to look at the property $failJob.ChildJobs[0].JobStateInfo.Reason.Message to get the message of what happened (as a string). 

$failJob.ChildJobs[0].JobStateInfo.Reason.Message
$jobError = $failJob.ChildJobs[0].JobStateInfo.Reason.Message
$jobError

There we go, got the error! 

Job State Completed

There are times when the job will complete, and the error message will indeed be in the ChildJob's Error property. Let's take the -ErrorAction Stop off the New-Item command and start the job again.

$failJob = Start-Job -Name FailJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory}

Now let's verify it shows as completed.

$failJob

This job has essentially the same error message as the other error example above. The difference is the error is stored in the ChildJob property of Error. This is one of the most confusing aspects of PowerShell jobs.  Receive-Job will display the error, but also like above not let you store it.  Not good for automation!

Here's how we get the error information this time.

$failJob.ChildJobs[0].Error
$jobError = $failJob.ChildJobs[0].Error
$jobError

Job Cleanup

Now that we've gone over how to create jobs, receive their information, and perform some error handling... let's move on to cleanup. 

The Remove-Job command will remove jobs from PowerShell. You can see this from start to finish with this example. 

$ourJob = Start-Job -Name CleanUp -ScriptBlock {Get-Process}
$ourJob
Get-Job

You can see the CleanUp job name amidst all of the other jobs I still have from this post. 

To remove this job only, you can use Remove-Job with the Job Name or Id. We can also pipe the variable we stored our job object in ($ourJob) to Remove-Job.

$ourJob | Remove-Job
Get-Job

And it's gone! What if we wanted to remove all of these jobs? You can actually pipe Get-Job to Remove-Job.

Get-Job | Remove-Job
Get-Job

As you can see, all of the jobs are now cleaned up.

Multiple Job Example

The glory of PowerShell Jobs is being able to run multiple commands at the same time. This example code will start up multiple jobs, and then use a While loop to monitor them. As they complete, the output and/or error messages will be output to a text file with the job name and date stamp as the file name. The output folder is C:\PowerShell\part10\output. 

Copy and paste the code in the ISE, and ensure you can write to C:\PowerShell\Part10. If you can't, you may see some nasty error messages, as I have not added any error handling to this example. Save the script as C:\PowerShell\Part10\part10.ps1.

Code

#Set the jobs variable to $true so the while loop processes at least once
$jobs         = $true
#Set the output folder path
$outputFolder = 'C:\PowerShell\part10\output'

#Create some jobs to monitor. One for each error handling example, and then one success that takes a minute to complete.
Start-Job -Name SleepProcess -ScriptBlock {Start-Sleep -Seconds 60; Get-Process}
Start-Job -Name FailJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory -ErrorAction Stop} 
Start-Job -Name FailCompletedJob -ScriptBlock {New-Item -Path 'Z:\' -Name 'test' -ItemType Directory} 

#If the folder doesn't exist, create it
If (!(Test-Path $outputFolder)) {

    New-Item -Path $outputFolder -ItemType Directory

}

#While $jobs = $true...
While ($jobs) { #Begin $jobs While Loop

    #Store the jobs in $ourJobs
    $ourJobs = Get-Job

    Write-Host "Checking for jobs..."

    #Use a ForEach loop to iterate through the jobs
    foreach ($jobObject in $ourJobs) { #Begin $ourJobs ForEach loop
        
        #Null out variables used in this loop cycle
        $jobResults   = $null
        $errorMessage = $null
        $jobFile      = $null
        $jobCommand   = $null

        #Store the command used in the job to display later
        $jobCommand   = $jobObject.Command

        #Use the Switch statement to take different actions based on the job's state value
        Switch ($jobObject.State) { #Begin Job State Switch

            #If the job state is running, display the job info
            {$_ -eq 'Running'} {

                Write-Host "Job: [$($jobObject.Name)] is still running..."`n
                Write-Host "Command: $jobCommand"`n

            }

            #If the job is completed, create the job file, say it's been completed, and then perform an error check
            #Then display different information if an error is found, versus successful completion
            #Use a here-string to create the file contents, then add the contents to the file
            #Finally use Remove-Job to remove the job
            {$_ -eq 'Completed'} {
                
                #Create file
                $jobFile = New-Item -Path $outputFolder -Name ("$($jobObject.Name)_{0:MMddyy_HHmm}.txt" -f (Get-Date)) -ItemType File

                Write-Host "Job [$($jobObject.Name)] has completed!"

                #Begin completed but with error checking...
                if ($jobObject.ChildJobs[0].Error) {

                    #Store error message in $errorMessage
                    $errorMessage = $jobObject.ChildJobs[0].Error | Out-String

                    Write-Host "Job completed with an error!"`n
                    Write-Host "$errorMessage"`n -ForegroundColor Red -BackgroundColor DarkBlue

                    #Here-string that contains file contents
                    $fileContents = @"
Job Name: $($jobObject.Name)

Job State: $($jobObject.State)

Command:

$jobCommand

Error:

$errorMessage
"@

                    #Add the content to the file
                    Add-Content -Path $jobFile -Value $fileContents

                } else {
                    
                    #Get job result and store in $jobResults
                    $jobResults = Receive-Job $jobObject.Name

                    Write-Host "Job completed without errors!"`n
                    Write-Host ($jobResults | Out-String)`n

                    #Here-string that contains file contents
                    $fileContents = @"
Job Name: $($jobObject.Name)

Job State: $($jobObject.State)

Command: 

$jobCommand

Output:

$($jobResults | Out-String)
"@

                    #Add content to file
                    Add-Content -Path $jobFile -Value $fileContents

                }

                #Remove the job
                Remove-Job $jobObject.Name
             
            }

            #If the job state is failed, state that it is failed and then create the file
            #Add the error message to the file contents via a here-string
            #Then use Remove-Job to remove the job
            {$_ -eq 'Failed'} {

                #Create the file
                $jobFile    = New-Item -Path $outputFolder -Name ("$($jobObject.Name)_{0:MMddyy_HHmm}.txt" -f (Get-Date)) -ItemType File
                #Store the failure reason in $failReason
                $failReason = $jobObject.ChildJobs[0].JobStateInfo.Reason.Message 

                Write-Host "Job: [$($jobObject.Name)] has failed!"`n
                Write-Host "$failReason"`n -ForegroundColor Red -BackgroundColor DarkBlue
                
                #Here-string that contains file contents
                $fileContents = @"
Job Name: $($jobObject.Name)

Job State: $($jobObject.State)

Command: 

$jobCommand

Error:

$failReason
"@
                #Add content to file
                Add-Content -Path $jobFile -Value $fileContents

                #Remove the job
                Remove-Job $jobObject.Name
            }


        } #End Job State Switch
     
    } #End $ourJobs ForEach loop

    #Clear the $ourJobs variable
    $ourJobs = $null

    #Get the new list of jobs as it may have changed since we did some cleanup for failed/completed jobs
    $ourJobs = Get-Job 

    #If jobs exists, keep the loop running by setting $jobs to $true, else set it to $false
    if ($ourJobs) {$jobs = $true} else {$jobs = $false}

    #Wait 10 seconds to check for jobs again
    Start-Sleep -Seconds 10

} #End $jobs While Loop

Here's the code in action...

So there's the output to the console. Let's check out the C:\PowerShell\Part10\Output folder.

That looks good. Let's take a peek at each file's contents.

And there you have it! You can do whatever you'd like with the job information in the Switch statement.

Homework

  • Jobs are capable of being run in different ways, including remotely. There are also a handful of WMI command that can be started as jobs. To see more ways jobs can be used, check out these resources.
  • Add some error handling to the script example in this post. 
    • Post ideas in the comments!

I hope you've enjoyed the series so far! As always, leave a comment if you have any feedback or questions!

-Ginger Ninja

[Back to Top]