Server work often starts with a few harmless clicks. Six months later, someone repeats them across 80 servers at 02:00. PowerShell turns those clicks into scripts you can test, review, schedule, and audit.
This guide installs PowerShell 7.6 LTS on Windows and macOS. It also covers Active Directory, services, disks, event logs, and Scheduled Tasks. The examples include dry runs, retry limits, logs, checks, and useful exit codes. Test them in a lab first. Active Directory is a poor place for improvisation.
What Is PowerShell?
PowerShell is Microsoft’s cross-platform shell, scripting language, and automation framework. Traditional shells tend to pass text between commands. PowerShell passes structured objects. You can filter a service by status or select an event by ID without parsing formatted columns.
That object pipeline works well with Windows services, Active Directory, event logs, Hyper-V, and remote management. It also makes scripts easier to inspect than long chains of text-processing commands.
PowerShell 7 is Microsoft’s current cross-platform product. This guide uses the 7.6 LTS branch. Windows PowerShell 5.1 remains built into supported Windows Server releases. Some older Windows modules still work better there. Both editions can live on the same machine without fighting over the furniture.
Scripts pay off when a task runs on a schedule or touches many targets. A GUI can be quicker for checking one server. A script gives you fixed inputs, repeatable output, and a record of each change. It also repeats mistakes perfectly. That’s why checks and dry runs matter.
Prerequisites
Make sure you have:
- A supported Windows release (PowerShell 7’s supported OS list extends beyond Windows Server 2022/2025 and Windows 11 to earlier supported Windows and Windows Server versions) or a supported macOS release; note that Windows Package Manager (winget) is included by default only on Windows 11 and Windows Server 2025, and can be installed separately on other supported Windows versions
- Local administrator rights for installation
- A supported PowerShell 7 release; this guide uses the 7.6 LTS branch
- Windows PowerShell 5.1 for modules that are not fully compatible with PowerShell 7
- A lab Active Directory domain for the directory examples
- Remote Server Administration Tools or the Active Directory module where required
- Task-specific permissions rather than unrestricted Domain Admin access
- A writable folder such as
C:\Automation\Logs - A code editor such as Visual Studio Code
- A backup or snapshot before testing directory changes
A basic management workstation needs only a few gigabytes of free storage. For a permanent admin station, a UPS and stable wired connection can reduce interruptions. A 2.5GbE switch won’t speed up a local script. It can stop shaky Wi-Fi from killing a remote session halfway through one.
Step-by-Step Guide
Step 1: Install PowerShell on Windows
Open Windows Terminal or Command Prompt as an administrator. Search for the application, right-click it, and select Run as administrator.
Confirm that Windows Package Manager is available:
winget --version
Expected output resembles:
v1.x.x
Search for the official package before installing it:
winget search --id Microsoft.PowerShell
Install Microsoft’s stable release:
winget install --id Microsoft.PowerShell --source winget
The --id flag selects the exact package ID. --source winget prevents other configured sources from supplying a package with a similar name. Package managers are useful, but fuzzy package selection is a needless gamble on an admin host.
Expected result:
Successfully installed
Close and reopen Windows Terminal so it picks up the updated PATH. Select PowerShell from the profile menu, or launch it directly:
pwsh
If winget isn’t available on Windows Server, use the official Windows installation guide. Download the supported x64 MSI unless the server uses ARM64. During setup, add PowerShell to PATH and register Windows event logging.
The MSI works fine, but it leaves updates to you unless another deployment tool handles them. Record the installed version in your patch inventory.
Step 2: Verify the Windows Installation
Don’t trust the installer’s success message alone. Open a fresh PowerShell 7 session and run:
$PSVersionTable
Expected fields include:
Name Value
—- —–
PSVersion 7.6.x
PSEdition Core
Platform Win32NT
PowerShell 7 reports Core under PSEdition. Windows PowerShell 5.1 reports Desktop. That detail matters when a module supports one edition but works badly in the other.
Check the executable path:
(Get-Command pwsh).Source
Typical output:
C:\Program Files\PowerShell\7\pwsh.exe
Use that absolute path in Scheduled Tasks. The scheduler may get a different PATH from your interactive account. “Works in my terminal” isn’t a useful monitoring state.
Step 3: Install and Verify PowerShell on macOS
If Homebrew isn’t installed, follow the current instructions at brew.sh. Don’t paste an installer command from a forum or an old internal wiki. Check bootstrap scripts like any other executable.
Open Terminal from Applications > Utilities, then install PowerShell:
brew install --cask powershell
Expected output ends with something similar to:
powershell was successfully installed!
Start PowerShell:
pwsh
Verify the installed edition and platform:
$PSVersionTable
Expected fields include:
PSVersion 7.6.x
PSEdition Core
Platform Unix
OS Darwin …
PowerShell on macOS handles cross-platform scripts and supported remote workflows well. It has no native access to Windows-only modules for Active Directory, Windows services, Event Log, or Scheduled Tasks. Run those commands on a Windows management host or through a remote path you’ve tested.
That boundary catches people because the shell looks the same. The operating system still decides which management APIs exist.
Step 4: Create a Safe Automation Workspace
Open an elevated PowerShell session on Windows. Create fixed folders for scripts, input files, reports, logs, and state data:
$folders = @(
'C:\Automation\Scripts',
'C:\Automation\Input',
'C:\Automation\Reports',
'C:\Automation\Logs',
'C:\Automation\State'
)
$folders | ForEach-Object {
New-Item -Path $_ -ItemType Directory -Force | Out-Null
}
-Force makes the command safe to rerun when a folder already exists. Piping to Out-Null hides five folder objects that tell us nothing useful.
Verify the result:
Get-ChildItem -Path 'C:\Automation' -Directory
Expected output lists all five directories.
Create a reusable logger at C:\Automation\Scripts\Common.ps1:
function Write-AutomationLog {
param(
[ValidateSet('INFO', 'WARN', 'ERROR')]
[string]$Level,
[Parameter(Mandatory)]
[string]$Target,
[Parameter(Mandatory)]
[string]$Message,
[string]$Path = 'C:\Automation\Logs\Automation.log'
)
$entry = '{0:u} [{1}] Target={2} Message="{3}"' -f `
(Get-Date), $Level, $Target, $Message
Add-Content -LiteralPath $Path -Value $entry -Encoding utf8
Write-Host $entry
}
This logger writes UTC-sortable timestamps, a fixed severity, the target, and the message. It’s plain by design. One text file works for a lab or a modest admin host. Concurrent jobs can mix entries, though. Long-running systems also need log rotation or central collection.
Load the function and generate a controlled error:
. 'C:\Automation\Scripts\Common.ps1'
Write-AutomationLog -Level INFO -Target 'LAB-SRV01' -Message 'Logger test succeeded'
try {
Get-Item -LiteralPath 'C:\Automation\Missing-Test-File.txt' -ErrorAction Stop
}
catch {
Write-AutomationLog -Level ERROR -Target 'LAB-SRV01' -Message $_.Exception.Message
}
The leading dot imports Common.ps1 into the current scope. -ErrorAction Stop turns the missing-file error into a terminating error, which catch can handle. You should see one INFO line and one ERROR line in the terminal and Automation.log.
Step 5: Preview Bulk Active Directory Changes
Confirm that the Windows Active Directory command is available before feeding it a CSV:
Get-Command Get-ADUser -ErrorAction Stop
If it’s missing, install the correct Microsoft administration tools for your Windows release. Then import the module:
Import-Module ActiveDirectory -ErrorAction Stop
Run a read-only query before making any changes:
Get-ADUser -Filter * -SearchBase 'OU=LabUsers,DC=lab,DC=example' `
-Properties Enabled |
Select-Object -First 10 Name, SamAccountName, Enabled
The -SearchBase flag limits the query to the lab OU. Select-Object -First 10 keeps the output readable. Check that the returned accounts belong to the intended scope. A valid query against the wrong OU is still wrong.
Create C:\Automation\Input\GroupChanges.csv:
SamAccountName,GroupName
lab.alex,Lab-File-Readers
lab.casey,Lab-File-Readers
Save this script as C:\Automation\Scripts\Set-LabGroupMembership.ps1:
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[string]$CsvPath = 'C:\Automation\Input\GroupChanges.csv'
)
$ErrorActionPreference = 'Stop'
. 'C:\Automation\Scripts\Common.ps1'
Import-Module ActiveDirectory -ErrorAction Stop
$rows = Import-Csv -LiteralPath $CsvPath
foreach ($row in $rows) {
$userName = $row.SamAccountName.Trim()
$groupName = $row.GroupName.Trim()
if ([string]::IsNullOrWhiteSpace($userName) -or
[string]::IsNullOrWhiteSpace($groupName)) {
Write-AutomationLog -Level WARN -Target 'CSV row' -Message 'Skipped incomplete input'
continue
}
try {
$user = Get-ADUser -Identity $userName -ErrorAction Stop
$group = Get-ADGroup -Identity $groupName -ErrorAction Stop
if ($PSCmdlet.ShouldProcess($user.SamAccountName, "Add to $($group.Name)")) {
Add-ADGroupMember -Identity $group -Members $user -ErrorAction Stop
Write-AutomationLog -Level INFO -Target $user.SamAccountName `
-Message "Added to group $($group.Name)"
}
}
catch {
Write-AutomationLog -Level ERROR -Target $userName `
-Message "Group change failed: $($_.Exception.Message)"
$global:HadFailure = $true
}
}
if ($global:HadFailure) { exit 1 }
exit 0
SupportsShouldProcess supplies -WhatIf and -Confirm. The script also checks for empty fields. It resolves each user and group before it calls Add-ADGroupMember. A malformed row gets skipped and logged instead of becoming a creative directory lookup.
Preview every proposed change without modifying Active Directory:
& 'C:\Automation\Scripts\Set-LabGroupMembership.ps1' -WhatIf
Expected result:
What if: Performing the operation “Add to Lab-File-Readers” on target “lab.alex”.
Check the CSV, search scope, users, groups, and preview. Then run the script with confirmation enabled:
& 'C:\Automation\Scripts\Set-LabGroupMembership.ps1' -Confirm
Writing this script takes longer than clicking through two user changes. It becomes safer and easier to audit when the same reviewed operation covers 50 or 500 accounts. The script doesn’t check existing membership first. Add that test if duplicate attempts create noisy logs in your environment.
Step 6: Check and Restart a Windows Service Safely
Save the following as C:\Automation\Scripts\Test-ServiceHealth.ps1:
param(
[string[]]$ServiceName = @('Spooler'),
[ValidateRange(0, 3)]
[int]$MaxRetries = 2
)
$ErrorActionPreference = 'Stop'
. 'C:\Automation\Scripts\Common.ps1'
$hadFailure = $false
foreach ($name in $ServiceName) {
try {
$service = Get-Service -Name $name -ErrorAction Stop
if ($service.Status -eq 'Running') {
Write-AutomationLog -Level INFO -Target $name -Message 'Service is healthy'
continue
}
Write-AutomationLog -Level WARN -Target $name `
-Message "Service state is $($service.Status)"
$recovered = $false
for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) {
Write-AutomationLog -Level WARN -Target $name `
-Message "Restart attempt $attempt of $MaxRetries"
Restart-Service -Name $name -ErrorAction Stop
(Get-Service -Name $name).WaitForStatus(
'Running',
[TimeSpan]::FromSeconds(20)
)
if ((Get-Service -Name $name).Status -eq 'Running') {
Write-AutomationLog -Level INFO -Target $name `
-Message "Recovered on attempt $attempt"
$recovered = $true
break
}
}
if (-not $recovered) {
throw "Service did not reach Running after $MaxRetries attempts"
}
}
catch {
Write-AutomationLog -Level ERROR -Target $name `
-Message "Health check failed: $($_.Exception.Message)"
$hadFailure = $true
}
}
if ($hadFailure) { exit 1 }
exit 0
The retry count is capped at three. Each attempt waits up to 20 seconds for the Running state. Those limits stop a broken service from turning one failed check into a permanent restart loop.
Test with a non-critical lab service:
& 'C:\Automation\Scripts\Test-ServiceHealth.ps1' `
-ServiceName 'Spooler' `
-MaxRetries 2
$LASTEXITCODE
Expected output is 0 when the service is healthy or recovered. It is 1 if any target fails. Your scheduler or monitoring system can act on $LASTEXITCODE.
Don’t use restart automation for database, storage, cluster, or security services before checking dependencies and vendor guidance. A restart can restore a failed service. It can also hide a recurring fault until the next maintenance window develops opinions.
Step 7: Generate a Scheduled Disk-Space Report
Save this as C:\Automation\Scripts\Get-DiskSpaceReport.ps1:
param(
[string[]]$ComputerName = @('LAB-SRV01', 'LAB-SRV02'),
[ValidateRange(1, 99)]
[double]$WarningPercent = 15
)
$ErrorActionPreference = 'Stop'
. 'C:\Automation\Scripts\Common.ps1'
$results = @()
$hadFailure = $false
foreach ($computer in $ComputerName) {
try {
$volumes = Get-CimInstance -ClassName Win32_LogicalDisk `
-ComputerName $computer `
-Filter 'DriveType=3' `
-ErrorAction Stop
foreach ($volume in $volumes) {
$freePercent = [math]::Round(
($volume.FreeSpace / $volume.Size) * 100,
2
)
$status = if ($freePercent -lt $WarningPercent) {
'WARNING'
}
else {
'OK'
}
$results += [pscustomobject]@{
Server = $computer
Volume = $volume.DeviceID
SizeGB = [math]::Round($volume.Size / 1GB, 2)
FreeGB = [math]::Round($volume.FreeSpace / 1GB, 2)
FreePercent = $freePercent
Status = $status
}
Write-AutomationLog -Level $(if ($status -eq 'OK') {'INFO'} else {'WARN'}) `
-Target "$computer $($volume.DeviceID)" `
-Message "Free space is $freePercent percent"
}
}
catch {
Write-AutomationLog -Level ERROR -Target $computer `
-Message "Disk query failed: $($_.Exception.Message)"
$hadFailure = $true
}
}
$reportPath = 'C:\Automation\Reports\DiskSpace-{0}.csv' -f (Get-Date -Format 'yyyy-MM-dd')
$results | Export-Csv -LiteralPath $reportPath -NoTypeInformation -Encoding utf8
$results | Format-Table -AutoSize
if ($hadFailure) { exit 1 }
exit 0
DriveType=3 limits the CIM query to local fixed disks. The script rounds free space to two decimal places. Any value below the default 15 percent threshold gets a WARNING status.
Run it manually before handing it to Task Scheduler:
& 'C:\Automation\Scripts\Get-DiskSpaceReport.ps1' `
-ComputerName 'LAB-SRV01','LAB-SRV02' `
-WarningPercent 15
Expected columns are Server, Volume, SizeGB, FreeGB, FreePercent, and Status. An unreachable server appears in the log as a failure instead of quietly vanishing from the result.
The script writes one CSV per date. Multiple runs on the same day overwrite that day’s file. Add hours and minutes to the filename if you need one file per run. Percentages can also mislead on large volumes: 5 percent free on a 20 TB volume is still 1 TB.
Step 8: Monitor Event Logs Without Duplicate Alerts
Save this as C:\Automation\Scripts\Watch-SystemEvents.ps1:
param(
[int[]]$EventId = @(6008, 7031),
[int]$LookbackMinutes = 15
)
$ErrorActionPreference = 'Stop'
. 'C:\Automation\Scripts\Common.ps1'
$statePath = 'C:\Automation\State\LastEventCheck.txt'
$now = Get-Date
$defaultStart = $now.AddMinutes(-$LookbackMinutes)
if (Test-Path -LiteralPath $statePath) {
$savedTime = Get-Content -LiteralPath $statePath -Raw
$startTime = [datetime]::Parse($savedTime)
}
else {
$startTime = $defaultStart
}
try {
$events = Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = $EventId
StartTime = $startTime
EndTime = $now
} -ErrorAction Stop |
Sort-Object TimeCreated, RecordId -Unique
foreach ($event in $events) {
Write-AutomationLog -Level WARN `
-Target "Event $($event.Id) Record $($event.RecordId)" `
-Message "$($event.ProviderName): $($event.Message)"
}
$now.ToString('o') | Set-Content -LiteralPath $statePath -Encoding ascii
exit 0
}
catch {
Write-AutomationLog -Level ERROR -Target 'System event log' `
-Message "Query failed: $($_.Exception.Message)"
exit 1
}
On its first run, the script looks back 15 minutes by default. Later runs start at the saved time. The state file advances only after a successful query. A failed check therefore keeps that window.
Run the query:
& 'C:\Automation\Scripts\Watch-SystemEvents.ps1' `
-EventId 6008,7031 `
-LookbackMinutes 15
Event ID 6008 records an unexpected shutdown. Event ID 7031 records a service termination. The saved time stops each run from searching the same broad window. RecordId helps remove duplicates from the returned set.
Narrow the event IDs, log names, providers, and time ranges before sending email or ticket alerts. Event logs are good evidence and terrible conversation partners when the filter is too broad.
Step 9: Package a Script as a Windows Scheduled Task
Open Server Manager > Tools > Task Scheduler. Select Task Scheduler Library, then click Create Task.
On General:
- Name the task
PowerShell Disk Space Report. - Select a dedicated managed service account or low-privilege service account.
- Choose Run whether user is logged on or not.
- Enable Run with highest privileges only if the script requires elevation.
- Select the correct Windows Server version under Configure for.
A dedicated identity makes permissions and audit records easier to follow. Avoid a personal admin account. Password changes and staff departures have a habit of becoming scheduled-task outages.
On Triggers, click New:
- Select On a schedule.
- Choose Daily.
- Set a low-impact time such as
06:00. - Ensure Enabled is checked.
Check what else runs at 06:00 before choosing it. Backups, antivirus scans, and inventory jobs can turn a cheap CIM query into needless contention.
On Actions, click New and enter:
- Program/script:
C:\Program Files\PowerShell\7\pwsh.exe - Add arguments:
-NoProfile -NonInteractive -File "C:\Automation\Scripts\Get-DiskSpaceReport.ps1" - Start in:
C:\Automation\Scripts
-NoProfile removes dependencies on a user’s interactive profile. -NonInteractive stops an unattended job from waiting for input. -File runs the named script. Absolute paths for the executable and script remove two common causes of scheduler-only failures.
On Conditions, clear power or network conditions that don’t fit the server workload. On Settings, enable Stop the task if it runs longer than and choose a limit, such as 30 minutes. Don’t allow overlapping runs unless the script handles concurrent writes and shared state.
Save the task, right-click it, and select Run. Confirm:
- Last Run Result is
0x0. - A new report exists in
C:\Automation\Reports. - New timestamped entries exist in
C:\Automation\Logs\Automation.log.
A 0x0 result proves only that the process returned success. Check the report and log too. A script can run perfectly while querying the wrong server list.
Configuration
Use these patterns for unattended automation you can diagnose at 03:00:
| Setting | Recommended pattern |
|---|---|
| PowerShell executable | Use the absolute pwsh.exe or powershell.exe path |
| Script paths | Use absolute paths under C:\Automation |
| Execution identity | Use a dedicated account with only the required rights |
| Profiles | Use -NoProfile for scheduled jobs |
| Prompts | Use -NonInteractive; scripts must never wait for input |
| Error behavior | Set $ErrorActionPreference = 'Stop' and use try/catch |
| Failure signal | Return exit 1 after logging a failed job |
| Logging | Include timestamp, severity, target, action, and error context |
| Retry policy | Set a small limit and stop after persistent failure |
| Secrets | Use managed identities, group managed service accounts, or a secret store |
| Script storage | Restrict write access; consider code signing for production |
You don’t need to change the machine-wide execution policy to install PowerShell. Check the effective policy at each scope before changing anything:
Get-ExecutionPolicy -List
Execution policy is a safety feature, not a strong security boundary. It can prevent accidental script runs. It won’t stop an attacker who already has permission to run code. Signed scripts, restricted folders, and controlled service identities provide better protection.
Tips and Troubleshooting
pwsh Is Not Recognized
Cause: The terminal was opened before installation, or the PowerShell directory isn’t present in PATH.
Fix: Close every terminal, open a new one, and run:
Get-Command 'C:\Program Files\PowerShell\7\pwsh.exe'
The absolute path separates a missing executable from a PATH problem. If the file is missing, reinstall PowerShell from Microsoft’s official package or MSI.
Get-ADUser Is Not Recognized
Cause: The Active Directory module isn’t installed, or PowerShell 7 can’t load the available module cleanly.
Fix: Check what is installed:
Get-Module -ListAvailable ActiveDirectory
Install or enable Microsoft’s Active Directory administration tools for that Windows release. If the module fails under PowerShell 7, test it in Windows PowerShell 5.1. Module compatibility remains one of the less charming parts of running both editions.
A Script Works Interactively but Fails When Scheduled
Cause: Scheduled Tasks use a different account, executable, environment, network context, or working directory.
Fix: Use absolute paths, -NoProfile, -NonInteractive, and a writable log folder. Confirm that the task identity can access every server, share, module, and file.
Avoid mapped drive letters. They usually don’t exist in unattended sessions because mappings belong to a user’s logon session. Use UNC paths such as \\fileserver\share and give the task identity direct access.
Task Scheduler Reports Success After the Script Failed
Cause: The script logged an error but returned exit code 0.
Fix: Track failures and end with a nonzero exit code:
if ($hadFailure) {
exit 1
}
exit 0
Task Scheduler can then tell success from failure. Logging alone won’t change Last Run Result. The process exit code does that job.
Service Restarts Keep Failing
Cause: The account lacks permission, a dependency is down, or the service has a configuration fault.
Fix: Stop retrying after the set limit. Record the full exception, inspect the related event log, and check dependencies:
Get-Service -Name 'Spooler' -DependentServices
Get-Service -Name 'Spooler' -RequiredServices
A third restart rarely fixes a permission problem. Escalate persistent failures with the captured error and dependency state. Increasing the retry number only delays the same result.
Event Monitoring Is Too Noisy
Cause: The query uses a broad log, time window, or severity filter.
Fix: Monitor explicit event IDs and a short time range. Save the last successful check time. Remove duplicate RecordId values before sending alerts.
Tune the query against a few days of real events before creating tickets. Otherwise, the first noisy morning teaches everyone to ignore the new alert source.
FAQ
Which Windows Server tasks are good automation candidates?
Good candidates follow clear rules, repeat often, and have simple success checks. Bulk directory changes, service checks, disk reports, event queries, certificate checks, Hyper-V inventory, backups, and health reports fit that model.
Keep unusual or high-risk changes behind manual approval. If you can’t write a clear success check, the task isn’t ready for a schedule.
How do I preview Active Directory changes?
Use a script with SupportsShouldProcess, then run it with -WhatIf. Check every user and group with read-only commands before calling a modifying cmdlet.
Remember that -WhatIf previews code paths. It can’t predict every directory policy, race, or replication issue that could affect the real change.
Which account should run a scheduled administrative script?
Use a dedicated account with the minimum local, directory, remote-management, and file permissions required. A group managed service account is often better than a normal user account because Windows manages its password.
Least privilege adds setup work because you must find each required permission. That 20-minute exercise is cheaper than giving every scheduled script unrestricted Domain Admin rights.
What differs between Windows PowerShell 5.1 and PowerShell 7?
Windows PowerShell 5.1 is the final built-in, Windows-only edition. It keeps broad support for older Windows modules. PowerShell 7 is cross-platform, gets current releases, and includes newer language and performance work.
Test each Windows-specific module before moving a production script from 5.1 to 7. A side-by-side install lets you move scripts one at a time.
Which examples require Windows-specific tools?
Get-ADUser, remote Windows service management with Get-Service, Get-WinEvent, Task Scheduler, and Windows management classes require Windows or a supported remote Windows management path. Active Directory commands also need the related administration module.
Can macOS run the same Windows Server scripts?
Usually, no. macOS can run PowerShell 7 and cross-platform scripts, but it has no native Windows services, Event Log, Scheduled Tasks, or Active Directory modules.
Use a Windows management host or a tested remote API or remoting workflow. Keeping Windows-specific code on Windows also makes module testing much less mysterious.
Wrapping Up
| Step | Action | Applies To |
|---|---|---|
| 1–3 | Install and verify PowerShell 7 | Windows and macOS |
| 4–8 | Build logged automation scripts | Windows Server |
| 9 | Schedule unattended jobs | Windows Server |
PowerShell earns its keep when a reviewed script replaces repeated, fragile clicks. Start with read-only reports and -WhatIf previews. Schedule the work after its logs, permissions, and failure paths survive lab testing.
Initial setup takes about 20–30 minutes, plus test time for each target system. After that, another server is usually one parameter or CSV row. Keep scripts boring, limits explicit, and exit codes useful. The person handling the next failed run will appreciate all three.