Script Blocks as First-Class Values
A script block, written as { ... }, is a chunk of PowerShell code stored as a value of type [scriptblock] that can be assigned to a variable, passed as a parameter, stored in a data structure, or invoked later with the call operator & or the .Invoke() method. This makes script blocks PowerShell's mechanism for passing behavior around, similar to lambdas or anonymous functions in other languages — cmdlets like Where-Object, ForEach-Object, and Invoke-Command all accept script blocks as their core building block for deferred execution.
Cricket analogy: Like a captain writing a bowling plan on a card before the match ({ bowl yorkers at the death }) and only 'invoking' it in the 19th over, the plan exists as a reusable object independent of when it's executed, similar to how death-over specialists are briefed in advance.
Delegates: Where-Object and ForEach-Object
Where-Object { $_.Status -eq 'Running' } and ForEach-Object { $_.Name } both receive a script block that's executed once per pipeline object, with $_ bound to the current item inside the block. Because the script block is just a value, you can also store it in a variable first ($filter = { $_.Status -eq 'Running' }) and reuse it across multiple pipelines, or pass it into a custom function's own parameter typed as [scriptblock] to build your own filtering or transformation logic.
Cricket analogy: Like reusing the same LBW-decision criteria script block across every match of a series rather than rewriting the umpiring logic each time, ensuring consistent decisions from Trent Bridge to the Gabba.
Variable Scopes: Global, Script, Local, Private
PowerShell scopes nest: global (the whole session), script (the currently running .ps1 file), local (the current function, script block, or the default when unspecified), and private (like local, but explicitly invisible to child scopes even though local variables are normally readable by children). A child scope can read a parent's variables by default but cannot write to them without an explicit scope modifier prefix like $global:varName or $script:varName; without that prefix, assigning inside a function creates a new local variable that merely shadows the outer one for the rest of that function's execution.
Cricket analogy: Like a franchise's team strategy (global scope) being visible to every match, while a specific game plan for today's match (script scope) only applies to that fixture, and a bowler's individual over plan (local scope) doesn't leak back to affect the team's season-long strategy unless explicitly reported up.
Closures: Capturing Variables with GetNewClosure
By default, a script block does not capture the value of outer variables at creation time — it resolves them by name when it eventually runs, so if the outer variable changes before invocation, the script block sees the new value. Calling .GetNewClosure() on a script block creates a bound copy that captures the current values of all outer variables at that moment, which is essential when generating multiple script blocks in a loop that must each remember a different value (like a distinct counter or item) rather than all referencing whatever the loop variable ended up as after the loop finished.
Cricket analogy: Like a bowler's plan referencing 'the current batsman' by role rather than a name — if the batsman changes mid-over the plan updates automatically — versus GetNewClosure() being like locking in a plan specifically against Virat Kohli regardless of who bats next.
function New-Multiplier {
param([int]$Factor)
# Without GetNewClosure(), every stored block would reference the
# same outer $Factor and behave identically once the loop finishes.
return { param($x) $x * $Factor }.GetNewClosure()
}
$multipliers = @{}
foreach ($n in 2, 3, 5) {
$multipliers[$n] = New-Multiplier -Factor $n
}
& $multipliers[3] 10 # 30, correctly bound to Factor = 3
& $multipliers[5] 10 # 50, correctly bound to Factor = 5
$script:runCount = 0
function Invoke-Tracked {
param([scriptblock]$Action)
$script:runCount++
& $Action
Write-Verbose "Total tracked invocations: $script:runCount"
}The dot-source operator (. ) runs a script block or .ps1 file in the CURRENT scope rather than creating a new child scope, which is how you load functions and variables from a helper script into your session. Compare this to the call operator (&), which always runs in a new child scope, isolating the callee's variables from the caller by default.
Assigning to a variable inside a function without a scope modifier never modifies a same-named variable in an outer scope — it silently creates a new local variable that shadows the outer one for the rest of that function, and the outer variable is left completely unchanged after the function returns. Use $script: or $global: explicitly if you intend to modify an outer-scope variable.
- Script blocks ({ ... }) are first-class [scriptblock] values that can be stored, passed, and invoked with & or .Invoke().
- Where-Object, ForEach-Object, and Invoke-Command all accept script blocks with $_ bound to the current pipeline item.
- PowerShell scopes nest: global, script, local (default), and private, with children able to read but not write parent variables by default.
- Use $global: or $script: scope modifiers to explicitly write to an outer scope from inside a function.
- Script blocks resolve outer variables by name at invocation time, not at creation time, unless bound with .GetNewClosure().
- .GetNewClosure() is essential when generating multiple script blocks in a loop that must each remember a distinct value.
- Dot-sourcing (. script.ps1) runs in the current scope; the call operator (&) always creates a new child scope.
Practice what you learned
1. What type of object is a PowerShell script block { ... }?
2. What happens if a function assigns to $x without a scope modifier when a global $x already exists?
3. By default, does a script block capture the current value of an outer variable at creation time or resolve it at invocation time?
4. What does .GetNewClosure() do?
5. How does dot-sourcing (. ./script.ps1) differ from calling a script normally with &?
Was this page helpful?
You May Also Like
Functions and Parameters
Learn to write reusable PowerShell functions with typed, validated, pipeline-aware parameters, and understand how advanced functions behave like native cmdlets.
Error Handling with Try/Catch
Understand PowerShell's terminating vs non-terminating errors and how try/catch/finally, -ErrorAction, and $ErrorActionPreference work together to handle failures gracefully.
Loops in PowerShell
Master PowerShell's iteration constructs — foreach, for, while, do-while, and pipeline-based ForEach-Object — along with break and continue for controlling repetition.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics