PowerShell’s Start-Job is a powerful tool for running commands and scripts asynchronously as background processes. However, its behavior, especially when combined with Wait-Job and -Credential, often leads to confusion.
This post demystifies how Start-Job works, why some installers fail silently inside jobs, and how to properly manage background tasks in PowerShell.
Assigning Start-Job to a Variable: Why It Matters
Consider these two commands. They both launch a background job to run a command prompt instruction.
# Snippet 1: No variable assignment
Start-Job -Name "installOffice" -ScriptBlock {
param($Cmd)
& cmd /c $Cmd
} -ArgumentList $InstallerCmd
# Snippet 2: Assigning the job to a variable
$job = Start-Job -Name "installOffice" -ScriptBlock {
param($Cmd)
& cmd /c $Cmd
} -ArgumentList $InstallerCmd
The functional difference is not about whether the job runs, but about how you interact with it.
- Without a Variable: The job starts, but I don’t have an immediate object reference to it. I have to retrieve it later using its name:
Get-Job -Name "installOffice". - With
$job = Start-Job: I capture the job object directly into the$jobvariable. This is the recommended approach for scripting because it makes it much easier to manage the job:- Check its status:
$job.State - Wait for it to complete:
Wait-Job -Job $job - Retrieve its output:
Receive-Job -Job $job
- Check its status:
The Wait-Job Misconception: “My Job Doesn’t Run!”
A common myth is that a job assigned to a variable won’t execute until you pipe it to Wait-Job. This is incorrect.
Start-Job always runs the job asynchronously and immediately.
I can verify this myself. I run the following command:
$job = Start-Job -Name "TestJob" -ScriptBlock { Start-Sleep 10; "done" }
Now, immediately check its status without waiting:
Get-Job -Name "TestJob"
The output will clearly show that the job’s state is Running. Wait-Job only pauses my script to wait for the job’s completion; it doesn’t start it.
The Real-World Gotcha: Why GUI Installers Fail in Jobs
So why does it sometimes seem like a job isn’t running, especially with installers like Microsoft Office?
The problem isn’t that the job is failing. The problem is that Microsoft Office and many other GUI-based setup executables are designed to fail silently when run in a non-interactive context.
A PowerShell job created with Start-Job runs in a hidden, non-interactive background session (Session 0 on Windows). This session has no desktop, no UI, and is isolated from your user session.
When you run an Office installer inside a job, especially with -Credential, it detects this non-interactive environment and simply exits without doing anything. You won’t see an error, a UI, or any output.
This is why piping to | Wait-Job seems to “fix” it for some commands but not for Office installers. The wait just makes you watch a job that is doing nothing.
The Correct Way to Run Installers
To run an installer like Office silently and reliably, I use tools designed for process execution, not background jobbing:
# Use Start-Process with the -Wait parameter
Start-Process -FilePath "setup.exe" -ArgumentList "/configure configuration.xml" -Wait
For true system-wide background work that needs to be robust, Scheduled Tasks are a much better alternative to Start-Job.
Understanding Job Visibility with Get-Job
Another key point is that Get-Job is both user-local and session-local.
- If User A starts a job, User B cannot see it with
Get-Job. - If I start a job in one PowerShell console, I can’t see it from another.
- Crucially, if I use
-Credentialto run a job as another user,Get-Jobin my current session will not see it, because it was created in a different user’s security context.
How to Find “Hidden” Job Processes
Since Get-Job is limited to its own session, how can I see all PowerShell jobs running on a system? I can’t see the “job” object itself, but I can see the powershell.exe processes that host them.
Use Get-CimInstance to find all powershell.exe processes and inspect their command lines.
# Find all PowerShell processes and their owners
Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe'" |
Select-Object ProcessId, CommandLine, CreationDate,
@{Name="Owner";Expression={(Invoke-CimMethod -InputObject $_ -MethodName GetOwner).User}}
# Filter for processes that were likely started by Start-Job
Get-CimInstance Win32_Process -Filter "Name='powershell.exe' AND CommandLine LIKE '%Start-Job%'" |
Select-Object ProcessId, CommandLine
This command gives me a system-wide view of the underlying processes that are executing PowerShell jobs.
Summary
- Assign to
$job:
Always assign Start-Job to a variable in scripts for easier management. It does not affect execution.
- Always Asynchronous:
Start-Job runs immediately and in the background, regardless of Wait-Job.
- Installers Fail Silently:
GUI-based installers like Microsoft Office(setup.exe) will not run in the non-interactive session created by Start-Job. Use Start-Process instead.
If Start-Process with -Credential caused UAC pop-up and cannot silently install, use Start-Job instead, and with -Credential to run as another user, and with -ArgumentList to pass the command to run.
Get-Jobis Local:
Get-Job only sees jobs created by the current user in the current session.
- Find Hidden Jobs:
Use Get-CimInstance Win32_Process to find the powershell.exe processes that are hosting background jobs.
- For Robust Tasks:
Use Scheduled Tasks for system-wide background work, not Start-Job.
- Why use start-job:
Start-Job is another way to run commands and scripts, if invoke-command or start-process are not working fine, all of them could use -Credential to run as another user, especially if need to elevate as admin user.
A working example:
$job = Start-Job -Name installOffice -Credential $AdminCred -ArgumentList $InstallerCmd -ScriptBlock {
param($Cmd)
& cmd /c $Cmd
} | Wait-Job
If Wait-Job is not missing, the background job might be failed silently.