PowerShell Remoting (WinRM, Invoke-Command, PSSessions)

PowerShell Intermediate/Advanced

Chapter 3 · PowerShell Remoting (WinRM, Invoke-Command, PSSessions)

Everything since Fundamentals 1 has run against the local machine. This chapter is where PowerShell stops being a single-machine tool: Invoke-Command runs a script block on another computer entirely and brings the results back — but "brings the results back" hides a real, important asterisk. What comes back across the network isn't the same kind of live object Fundamentals 1 built this whole course around; understanding exactly what's lost in that trip is this chapter's own central lesson.

What Remoting Actually Is: WinRM

PowerShell Remoting runs over WinRM (Windows Remote Management), Microsoft's implementation of the WS-Management protocol — a separate transport from SSH, though PowerShell 7+ can optionally use SSH instead once it's configured on both ends. One-time setup, run as Administrator on the machine you want to remote into:

Enable-PSRemoting -Force

This starts the WinRM service, creates a listener for incoming remoting connections, and opens the necessary firewall rule — all three are required before any of the cmdlets below will succeed against that machine.

Invoke-Command: Running Code on a Remote Machine

Invoke-Command -ComputerName Server01 -ScriptBlock { Get-Service -Name Spooler }

The script block runs entirely on Server01, not on the local machine — and only the results travel back over the network, not a live connection to keep working with them afterward.

The Deserialization Gotcha: What Comes Back Isn't "Live"

$procs = Invoke-Command -ComputerName Server01 -ScriptBlock { Get-Process } $procs[0].GetType().Name # Deserialized.System.Diagnostics.Process — NOT the real type anymore $procs[0].ProcessName # works fine — the data survived the trip $procs[0].Kill() # fails — Kill() is a METHOD, and methods don't survive the trip
The central fact this chapter is built on
A remote object gets serialized into XML to cross the network, then deserialized back into a PowerShell object on your local machine — but deserialization can only reconstruct data (properties), not behavior (methods). The real, live System.Diagnostics.Process object, and its ability to actually terminate a running process, never left Server01 at all — what you're holding locally is a detailed, read-only snapshot, type-prefixed Deserialized. as an explicit signal of exactly that. This is a deliberate architectural boundary, not a limitation to work around: letting a remote object's live methods execute back on your machine (or worse, letting your local methods silently execute back on the remote one) would be a genuine security problem, not a convenience worth having.

Passing Local Data In: the $using: Scope Modifier

A remote script block runs in a completely separate process on a completely separate machine — it has no visibility into local variables by default, even ones defined on the very line above the call:

$serviceName = "Spooler" # WITHOUT $using: — fails, or silently sees $null, because $serviceName doesn't exist remotely Invoke-Command -ComputerName Server01 -ScriptBlock { Get-Service -Name $serviceName } # WITH $using: — explicitly injects the LOCAL value into the remote script block Invoke-Command -ComputerName Server01 -ScriptBlock { Get-Service -Name $using:serviceName }

$using: is the explicit signal "reach across and grab this value from my local session" — without it, the remote script block only ever sees its own, entirely empty, local scope.

Persistent Connections: New-PSSession & State

$session = New-PSSession -ComputerName Server01 Invoke-Command -Session $session -ScriptBlock { $data = Get-Process } Invoke-Command -Session $session -ScriptBlock { $data.Count } # $data is STILL there — the session remembered it Remove-PSSession $session
-ComputerName is stateless — every call is a brand-new, throwaway connection
Invoke-Command -ComputerName opens a fresh connection, runs the script block, and tears the whole thing down again — every single time. A variable set in one -ComputerName call is completely gone by the next one, even against the identical computer, because there was never a persistent session for it to survive in:
Invoke-Command -ComputerName Server01 -ScriptBlock { $data = 1 } Invoke-Command -ComputerName Server01 -ScriptBlock { $data } # $null — a completely new, unrelated connection
Reach for New-PSSession the moment state needs to survive between calls — it's the difference between one-shot remote commands and an actual ongoing remote working session.

Enter-PSSession -ComputerName Server01 is the fully interactive version of the same idea — it drops your prompt directly into the remote machine's own PowerShell session, exactly as if you'd logged in locally, until Exit-PSSession (or plain exit) returns you home.

Multiple Computers at Once — Automatic Parallelism

Invoke-Command -ComputerName Server01, Server02, Server03 -ScriptBlock { Get-Service Spooler } # Runs against all three simultaneously by default, not one after another — # -ThrottleLimit controls the maximum number running concurrently (default 32)
A first practical habit
Before relying on a property from a remote result, check .GetType().Name the way this chapter's own $procs[0] example did — seeing the Deserialized. prefix is an immediate, reliable signal that only data, not behavior, made the trip back.

Hands-On Exercises

Exercise 1

Explain why $procs[0].Kill() fails after Invoke-Command -ComputerName Server01 -ScriptBlock { Get-Process }, even though $procs[0].ProcessName works perfectly fine. Use this chapter's own deserialization explanation.

📄 View solution
Exercise 2

Explain why $serviceName isn't visible inside a remote script block without $using:serviceName, even though it's clearly defined in the local session on the line right before the Invoke-Command call.

📄 View solution
Exercise 3

Explain why a variable set inside one Invoke-Command -ComputerName call doesn't persist into a second Invoke-Command -ComputerName call against the same computer. What would you change to make it persist?

📄 View solution

Chapter 3 Quick Reference

  • WinRM / WS-Management — the protocol underneath PowerShell Remoting; Enable-PSRemoting -Force is the one-time setup
  • Invoke-Command -ComputerName / -ScriptBlock — runs code remotely, returns only results, no persistent connection
  • Deserialized objects — remote results keep their data (properties) but lose their behavior (methods); type-prefixed Deserialized.
  • $using:variableName — explicitly injects a local value into a remote script block's otherwise-empty scope
  • New-PSSession / -Session — a persistent connection; variables and state survive between multiple Invoke-Command calls
  • -ComputerName alone is stateless — every call is a brand-new connection; nothing persists between calls
  • Enter-PSSession — a fully interactive remote prompt; Exit-PSSession to return
  • Multiple computers run in parallel automatically-ThrottleLimit controls the maximum concurrent (default 32)
  • Next chapter: Background & Parallel Execution (Start-Job, ForEach-Object -Parallel, a Runspaces intro)