My typical PowerShell Script header
Every script I write starts with the same header. It documents what the script does, who wrote it, and when — and it sets up the standard parameters and error handling that I use consistently.
The template
#Requires -Version 7.2
#Requires -Modules Az.Accounts
<#
.SYNOPSIS
Brief one-line description of what the script does.
.DESCRIPTION
Longer description. What problem does this solve?
What are the prerequisites?
.PARAMETER ParameterName
Description of the parameter.
.PARAMETER Environment
Target environment: Dev, Staging, or Prod.
.EXAMPLE
.\ScriptName.ps1 -ParameterName "value" -Environment Dev
Runs the script against the Dev environment.
.EXAMPLE
.\ScriptName.ps1 -ParameterName "value" -Environment Prod -WhatIf
Shows what would happen without making any changes.
.LINK
https://scottmcgrath.com
.NOTES
Author: Scott McGrath
Created: 2023-07-20
Version: 1.0 — Initial release
License: MIT — This script is provided "as is", without warranty of any kind,
express or implied. The author accepts no responsibility for any damage,
data loss, or other consequences resulting from its use. Use at your
own risk.
#>
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$ParameterName,
[Parameter()]
[ValidateSet('Dev', 'Staging', 'Prod')]
[string]$Environment = 'Dev'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$scriptRoot = $PSScriptRoot
Why each part matters
#Requires
#Requires lines go above everything else, including the comment block. PowerShell evaluates them before running any code, so the script fails immediately with a clear error message rather than crashing midway through with something cryptic.
#Requires -Version 7.2 # Enforces a minimum PS version
#Requires -Modules Az.Accounts # Ensures the module is installed
#Requires -RunAsAdministrator # Fails fast if not elevated
I don’t always need all three, but I always consider them. For anything touching Azure I include the module requirement. For anything writing to system paths or the registry, -RunAsAdministrator.
Comment-based help
The .SYNOPSIS, .DESCRIPTION, .PARAMETER, .EXAMPLE, and .LINK blocks feed directly into Get-Help. Anyone who runs Get-Help .\ScriptName.ps1 -Full gets proper documentation — including examples they can copy and run.
.EXAMPLE is the most underused block. I try to include at least two: a normal run and a -WhatIf run. The text below each example is surfaced by Get-Help as a description, so make it useful.
I track version history in .NOTES as a short changelog:
Version: 1.0 — Initial release
1.1 — Added -Environment parameter
1.2 — Fixed token refresh on long-running operations
This keeps the history with the script rather than relying on git blame alone.
[CmdletBinding(SupportsShouldProcess)]
This single attribute gives your script -WhatIf and -Confirm support as common parameters — you don’t declare them yourself in the param block. To honour them, wrap any destructive operation with $PSCmdlet.ShouldProcess:
foreach ($resource in $resources) {
if ($PSCmdlet.ShouldProcess($resource.Name, 'Delete')) {
Remove-AzResource -ResourceId $resource.Id -Force
}
}
When a caller passes -WhatIf, ShouldProcess returns $false and prints What if: Performing the operation "Delete" on target "my-resource" — no changes are made. Without ShouldProcess wrapping the call, -WhatIf silently does nothing even though CmdletBinding is declared.
Parameter validation
Validation attributes belong in the param block, not in the script body. They run before any of your code does.
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$ParameterName
[Parameter()]
[ValidateSet('Dev', 'Staging', 'Prod')]
[string]$Environment = 'Dev'
[Parameter()]
[ValidateRange(1, 100)]
[int]$RetryCount = 3
[Parameter()]
[ValidateScript({ Test-Path $_ })]
[string]$ConfigPath
ValidateSet is particularly useful — it enables tab completion on the parameter value, which makes the script much nicer to call interactively.
Set-StrictMode -Version Latest
Catches uninitialized variables, deprecated syntax, and other common mistakes at runtime instead of silently misbehaving. The two most common things it catches in practice: referencing a variable you’ve mistyped, and accessing a property on $null.
$ErrorActionPreference = 'Stop'
Turns non-terminating errors into terminating ones, so the script stops on any error rather than continuing with bad state. Most of the time this is what you want — especially for scripts that chain Azure API calls where a silent failure in step 2 corrupts step 3.
The caveat: some cmdlets and native executables write to the error stream even on success. robocopy is the classic example. For those calls, either reset the preference locally or catch and inspect the error:
try {
Remove-AzResourceGroup -Name $rgName -Force
}
catch {
Write-Error "Failed to remove resource group '$rgName': $_"
throw
}
$PSScriptRoot
$PSScriptRoot is the directory the script file lives in. Capture it at the top before anything else runs, and use it for all paths relative to the script:
$configPath = Join-Path $PSScriptRoot 'config.json'
$logPath = Join-Path $PSScriptRoot 'logs' 'run.log'
Hard-coding relative paths like .\config.json breaks the moment the script is called from a different working directory. $PSScriptRoot doesn’t.
A note on param blocks
Always declare your parameters explicitly. Avoid $args. Named parameters make scripts self-documenting, enable tab completion, and allow -WhatIf and -Confirm to work correctly. Add [Mandatory] for anything the script cannot run without, and always set a sensible default for optional parameters so the script is safe to run interactively without supplying every argument.
Licence
I include an MIT licence notice in every script I share publicly. The key part is the warranty disclaimer — scripts that modify system configuration, Active Directory, Exchange, or Azure resources can cause real damage if something goes wrong, and it’s important to be clear that anyone running your code does so at their own risk.
The MIT licence is a good fit because it’s widely understood, permissive, and its “as is” clause covers exactly this: no warranty, no liability, use at your own risk. You don’t need a full LICENSE file for a standalone script — a one-liner in .NOTES is enough to make the intent clear.