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 I don’t have an immediate object reference to it. I have to retrieve it later using its name: Get-Job -Name "installOffice".
  2. With $job = Start-Job: I 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.

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 -Credential to run a job as another user, Get-Job in 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-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.