PowerShell Script Template: Boilerplate for Production Scripts

PowerShell Script Template: Boilerplate for Production Scripts

PowerShell Tips Editor 1 min read
PowerShell Script Template: Boilerplate for Production Scripts

Every production PowerShell script you write will benefit from the same structural elements: comment-based help, parameter validation, error handling, and cleanup logic. Starting from a solid PowerShell script template means you never accidentally ship a script without parameter documentation, never forget a try/catch, and always follow exit code conventions your calling systems expect. This post provides a copy-paste-ready template you can adapt for any automation task.

Quick Answer / TL;DR

Start every script with a comment-based help block, then a param() block as the first executable statement, then try/catch/finally for the main logic. Add #Requires statements for module dependencies.

Comment-Based Help Header

Comment-based help makes your script a first-class cmdlet: Get-Help .\Script.ps1 produces formatted documentation, and tab-completion in the PowerShell ISE and VS Code shows parameter descriptions. The .SYNOPSIS, .DESCRIPTION, .PARAMETER, and .EXAMPLE sections are the minimum you should provide.

<#
.SYNOPSIS
    One-line description of what the script does.

.DESCRIPTION
    Detailed description of the script's purpose, assumptions, and side effects.
    Include any prerequisites or environment requirements.

.PARAMETER TargetServer
    The name or IP address of the server to operate on.

.PARAMETER LogPath
    Directory where log files will be written. Created if it does not exist.

.EXAMPLE
    .\Invoke-Deployment.ps1 -TargetServer 'web01' -LogPath 'C:\Logs'
    Runs the deployment against web01 and logs to C:\Logs.

.NOTES
    Author:  Your Name
    Created: 2024-03-15
    Requires: PowerShell 5.1+, ActiveDirectory module
#>

Parameter Block with Validation

The param() block must be the first executable statement — before any module imports, variable assignments, or other code. [CmdletBinding()] enables common parameters (-Verbose, -WhatIf, -ErrorAction). Add validation attributes to catch bad inputs before the script logic runs.

#Requires -Version 5.1
#Requires -Modules ActiveDirectory

[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory, HelpMessage = 'Target server name or IP')]
    [ValidateNotNullOrEmpty()]
    [string]$TargetServer,

    [Parameter()]
    [ValidateSet('Dev','Staging','Production')]
    [string]$Environment = 'Production',

    [Parameter()]
    [ValidateScript({ Test-Path $_ -PathType Container })]
    [string]$LogPath = 'C:\Logs',

    [Parameter()]
    [switch]$Force
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Logging Setup

Initialize the log file early, before any logic that might fail. Use a timestamped filename so multiple runs do not overwrite each other. This section also imports any modules needed by the main logic.

# Logging setup
$scriptName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Path)
$timestamp  = Get-Date -Format 'yyyyMMdd_HHmmss'
$logFile    = Join-Path $LogPath "$scriptName-$timestamp.log"

function Write-Log {
    param([string]$Message, [string]$Level = 'INFO')
    $line = "$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ' -AsUTC 2>$null ?? (Get-Date -Format 'yyyy-MM-ddTHH:mm:ss')) [$Level] $Message"
    Add-Content -Path $logFile -Value $line
    Write-Verbose $line
}

New-Item -Path $LogPath -ItemType Directory -Force | Out-Null
Write-Log "Script started. Target: $TargetServer, Environment: $Environment"

Try-Catch Main Logic

Wrap the main logic in a try block. With $ErrorActionPreference = 'Stop', all errors become terminating errors caught by the catch block. This ensures no error goes unnoticed. Log errors with full exception details including the stack trace for debugging.

try {
    Write-Log "Connecting to $TargetServer"

    # Example: check server reachability first
    if (-not (Test-Connection -ComputerName $TargetServer -Count 1 -Quiet)) {
        throw "Cannot reach $TargetServer. Check network connectivity."
    }

    Write-Log "Server $TargetServer is reachable"

    # Main logic — use ShouldProcess for destructive operations
    if ($PSCmdlet.ShouldProcess($TargetServer, "Deploy to $Environment")) {
        Write-Log "Starting deployment to $Environment on $TargetServer"
        # ... deployment code here ...
        Write-Log 'Deployment completed successfully'
    }

} catch {
    Write-Log "FATAL: $($_.Exception.Message)" -Level 'ERROR'
    Write-Log "Stack trace: $($_.ScriptStackTrace)" -Level 'ERROR'
    Write-Error $_.Exception.Message
    exit 1
}

Finally Cleanup Block

The finally block runs whether the script succeeds or fails. Use it for cleanup that must always happen: closing database connections, releasing file handles, removing temp files, and writing the final log entry. Never put critical business logic in finally — only cleanup.

finally {
    # Always runs — cleanup here
    Write-Log 'Cleanup: releasing resources'

    # Remove temp files if they exist
    if (Test-Path "$env:TEMP\$scriptName-*") {
        Get-ChildItem "$env:TEMP\$scriptName-*" | Remove-Item -Force -ErrorAction SilentlyContinue
    }

    Write-Log "Script finished. Log: $logFile"
}

Exit Code Conventions

Exit codes communicate script outcome to calling systems (Task Scheduler, CI pipelines, monitoring tools). The standard convention: 0 = success, 1 = error, 2+ = specific error categories. Always exit with an explicit code rather than letting PowerShell exit 0 by default on unhandled errors.

# Exit code conventions
# Exit 0 — success (implicit at end of successful script)
# Exit 1 — general unhandled error (in catch block)
# Exit 2 — configuration/parameter error
# Exit 3 — dependency not found

# Example: explicit success exit
Write-Log 'All operations completed successfully'
exit 0

# Task Scheduler: check last run result
# A task result of 0x0 (0) = success
# A task result of 0x1 (1) = error

# CI pipeline integration
$LASTEXITCODE  # check after calling this script from another

Common Errors and Fixes

  • Missing #Requires statement for module dependencies. Without #Requires -Modules ActiveDirectory, the script runs on machines without the module and fails deep in the logic with a cryptic “cmdlet not found” error. #Requires checks at parse time before any code executes, producing a clear error message immediately.
  • Param block must be first executable statement. Putting Import-Module, Set-Location, or any other statement before the param() block causes PowerShell to treat the parameters as ordinary code rather than script parameters, breaking parameter binding. Always put the [CmdletBinding()] and param() block absolutely first in the script body.

Related Cmdlets / See Also

Wrapping Up

This template covers the structural elements every production script needs. Copy it, fill in your logic, and you ship scripts with built-in documentation, parameter validation, error handling, and consistent exit codes from day one. The investment in structure pays off the first time a scheduled task fails and your detailed log file tells you exactly why.

Send-Item -To