Scripting with PowerShell Runspaces
A great deal of what we do in PowerShell is based on the use of runspaces. These are are configured containers, not in the Docker sense, that allow us to run PowerShell commands. Normally, we never have to think about them. But they are there. If you open a clean PowerShell session, there is always at least one runspace.
PS C:\> Get-Runspace
Id Name ComputerName Type State Availability
-- ---- ------------ ---- ----- ------------
1 Runspace1 localhost Local Opened Busy
Fortunately, you don't have to do anything with this. When you run a job command, or Invoke-Command
, PowerShell will create additional runspaces. Again, you don't have to worry about this. PowerShell will manage the runspaces for you.
But what about when you do want to work with a custom runspace? Are there any advantages? How do you create a runspace? What can you do with it? These are a few of the questions I want to tackle over the next few articles.
Creating a Runspace
One type of runspace that you can create is a System.Management.Automation.PowerShell
object.
Let's take a peek using my Get-TypeMember
function.
PS C:\> Get-TypeMember System.Management.Automation.PowerShell
Type: System.Management.Automation.PowerShell
Name MemberType ResultType IsStatic IsEnum
---- ---------- ---------- -------- ------
InvocationStateChanged Event
AddArgument Method PowerShell
AddCommand Method PowerShell
AddParameter Method PowerShell
AddParameters Method PowerShell
AddScript Method PowerShell
AddStatement Method PowerShell
BeginInvoke Method IAsyncResult
BeginStop Method IAsyncResult
Connect Method Collection`1
ConnectAsync Method IAsyncResult
Create Method PowerShell True
CreateNestedPowerShell Method PowerShell
EndInvoke Method PSDataCollection`1
EndStop Method Void
GetSteppablePipeline Method SteppablePipeline
GetType Method Type
Invoke Method Void
Invoke Method Collection`1
Invoke Method Collection`1
InvokeAsync Method Task`1
Stop Method Void
StopAsync Method Task
Commands Property PSCommand
HadErrors Property Boolean
HistoryString Property String
InstanceId Property Guid
InvocationStateInfo Property PSInvocationStateInfo
IsNested Property Boolean
IsRunspaceOwner Property Boolean
Runspace Property Runspace
RunspacePool Property RunspacePool
Streams Property PSDataStreams
We have a few options for creating a PowerShell runspace.
PS C:\> [powershell]::create.OverloadDefinitions
static powershell Create()
static powershell Create(System.Management.Automation.RunspaceMode runspace)
static powershell Create(initialsessionstate initialSessionState)
static powershell Create(runspace runspace)
Let's create a simple example.
PS C:\> $ps = [powershell]::create()
PS C:\> $ps
Commands : System.Management.Automation.PSCommand
Streams : System.Management.Automation.PSDataStreams
InstanceId : 9a301a76-d55f-4ada-8500-3d4047b01dfe
InvocationStateInfo : System.Management.Automation.PSInvocationStateInfo
IsNested : False
HadErrors : False
Runspace : System.Management.Automation.Runspaces.LocalRunspace
RunspacePool :
IsRunspaceOwner : True
HistoryString :
At the moment, this is an empty runspace.
PS C:\> $ps.commands
Commands
--------
{}
Adding a Command
However, we can add commands to it.
$ps.AddCommand('Get-ChildItem', $true)
The second parameter is a boolean that indicates whether the command should use the local scope. We aren't adding a pipelined expression. Only a single command. Later, I'll show you how to add a script file.
Adding Parameters
Next, you might want to add some parameters to this command. I'm also going to define a local variable.
$p = 'c:\work'
$ps.AddParameter('Path', $p)
$ps.AddParameter('Recurse', $True)
Here's what the runspace looks like thus far.
PS C:\> $ps.Commands.commands
Parameters : {Path, Recurse}
CommandText : Get-ChildItem
IsScript : False
UseLocalScope : True
CommandOrigin : Runspace
IsEndOfStatement : False
MergeUnclaimedPreviousCommandResults : None
PS C:\> $ps.Commands.commands.parameters
Name Value
---- -----
Path c:\work
Recurse True
Invoking the Runspace
To run the command, we need to invoke the runspace.
$ps.Invoke()

As far as the runspace is concerned, there is nothing left to do.
PS C:\> $ps.InvocationStateInfo
State Reason
----- ------
Completed
However, you can invoke the runspace as often as you need.
PS C:\> $PS.Invoke() | Measure length -sum | select count,sum
Count Sum
----- ---
619 270597236.00
If I check my list of runspaces, I'll see this one is still open.
PS C:\> get-runspace
Id Name ComputerName Type State Availability
-- ---- ------------ ---- ----- ------------
1 Runspace1 localhost Local Opened Busy
4 Runspace4 localhost Local Opened Available
If you are finished with the runspace you should clean it up.
$ps.Dispose()
This will close the runspace and remove it from the list.
PS C:\> get-runspace
Id Name ComputerName Type State Availability
-- ---- ------------ ---- ----- ------------
1 Runspace1 localhost Local Opened Busy
More Parameters
Let me show you another way to add parameters using a hashtable. Here's a new runspace.
$rsCim = [powershell]::create()
[void]$rsCim.AddCommand('Get-CimInstance')
The AddCommand()
method writes the runspace object to the pipeline. Using [void]
suppresses the output. Next, I'll add a hashtable of parameters.
$params = @{
ClassName = 'Win32_OperatingSystem'
Computername = 'Dom1', 'Srv1', 'Srv2'
Verbose = $true
}
This is the same technique you would use with splatting. Use the AddParameters()
method to add the parameters hashtable
[void]$rsCim.AddParameters($params)
I can easily verify them.
PS C:\> $rsCim.Commands.Commands.parameters
Name Value
---- -----
Computername {Dom1, Srv1, Srv2}
Verbose True
ClassName Win32_OperatingSystem
Let's invoke the runspace.
PS C:\> $rsCim.Invoke() | Select-Object CSName, Organization, Caption
CSName Organization Caption
------ ------------ -------
SRV2 Company.pri Microsoft Windows Server 2019 Standard
DOM1 Company.pri Microsoft Windows Server 2019 Standard
SRV1 Company.pri Microsoft Windows Server 2019 Standard
Once you've defined the command and parameter, you cannot modify them. You need to dispose of the runspace and recreate it.
This example is querying all computers in a single runspace. If you have a large number of computers, you might want to create a runspace for each computer and invoke the runspaces asynchronously. We'll look at that later.
Using a Script File
Let's wrap up today by looking at using a script file. I have a simple script file that I want to run.
#requires -version 5.1
#this is a sample PowerShell script
Param([int]$Count = 1)
Write-Host "this is a sample script that doesn't do anything but write a random number" -ForegroundColor Cyan
Get-Random -Minimum 1 -Maximum 1000 -count $Count
As before, I'll create my PowerShell runspace.
$ps = [powershell]::create()
Instead of adding a command, I'll add the contents of a script file.
$c = Get-Content 'c:\scripts\samplescript.ps1' -Raw
$ps.AddScript($c)
I think you'll have best results using the -Raw
parameter. Otherwise, you'll get an array of strings.
I can also add parameters for the script.
$ps.AddParameter('Count', 5)
When I'm ready, I can invoke it.
PS C:\> $ps.Invoke()
214
599
334
203
372
That is the pipeline output from the script file. But where is the Write-Host
output? It's in the Information
stream.
PS C:\> $ps.streams
Error : {}
Progress : {}
Verbose : {}
Debug : {}
Warning : {}
Information : {this is a sample script that doesn't do anything but write a random number}
If you have errors, this is where they will be captured. We'll look at that next time.
Summary
Don't feel you have to begin scripting with runspaces. You can derive many of the benefits from using background or thread jobs. But, some of you may want to build advanced tooling that takes advantage of runspaces. I have a few other things to show you next time that might pique your interest. In the meantime, try my code samples and see what ideas they spark.