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.

  1. Without a Variable: The job starts, but you don’t have an immediate object reference to it. You have to retrieve it later using its name: Get-Job -Name "installOffice".
  2. With $job = Start-Job: You capture the job object directly into the $job variable. 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

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.

You can verify this yourself. 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 your 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, you should 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 you start a job in one PowerShell console, you can’t see it from another.
  • Crucially, if you use -Credential to run a job as another user, Get-Job in your 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 you see all PowerShell jobs running on a system? You can’t see the “job” object itself, but you 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 you 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-Job is 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.