PowerShell Home Lab Toolkit for Backup Freshness, Service Health, and Daily Status
Build a small PowerShell operations toolkit that records backup-file freshness, checks selected Windows services, writes durable daily evidence, and runs on a controlled schedule.
Expected Outcome
A parameterized PowerShell script that inventories recent backup artifacts without claiming restore verification, records selected service states without changing them by default, exports a timestamped status report, and can run daily under a deliberately chosen Task Scheduler identity.
Assumptions
Windows 10/11 or Windows Server with Windows PowerShell 5.1 or PowerShell 7 available for the script logic.
Read access to the backup directory or share whose file freshness you want to inspect.
A defined freshness threshold that matches the expected backup cadence; a seven-day threshold is only an example.
A short list of Windows service names that matter in your lab and an understanding of whether the scheduled task identity can query them.
An output directory where the scheduled task identity can create status files.
Bill of Materials
A script directory such as C:\Scripts.
A backup folder or UNC path containing backup artifacts that can be dated reliably.
A report directory such as C:\OpsEvidence\HomeLabStatus.
Task Scheduler or schtasks.exe for recurring execution.
Build Steps
- Define the evidence scope
Choose the backup location, filename pattern, freshness threshold, services, and report directory before writing the script. File age can show whether expected backup artifacts are arriving; it does not prove that those artifacts are valid or restorable.
Configuration or code example: review values for your environment
$BackupPath = 'C:\Backups' $BackupFilter = '*.bak' $MaxAgeHours = 24 $ServiceNames = @('Spooler') $ReportPath = 'C:\OpsEvidence\HomeLabStatus' - Collect backup freshness evidence
Inventory matching files and calculate age from LastWriteTime. Preserve the newest matching artifact as evidence and report a missing-artifact state separately from an old-artifact state.
Configuration or code example: review values for your environment
$Now = Get-Date $BackupFiles = Get-ChildItem -Path $BackupPath -Filter $BackupFilter -File -ErrorAction Stop | Sort-Object LastWriteTime -Descending $NewestBackup = $BackupFiles | Select-Object -First 1 $BackupEvidence = if ($null -eq $NewestBackup) { [pscustomobject]@{ Status='Missing'; Path=$BackupPath; File=$null; LastWriteTime=$null; AgeHours=$null; ThresholdHours=$MaxAgeHours } } else { $AgeHours = [math]::Round(($Now - $NewestBackup.LastWriteTime).TotalHours, 1); [pscustomobject]@{ Status=if ($AgeHours -le $MaxAgeHours) { 'Fresh' } else { 'Stale' }; Path=$BackupPath; File=$NewestBackup.FullName; LastWriteTime=$NewestBackup.LastWriteTime; AgeHours=$AgeHours; ThresholdHours=$MaxAgeHours } } - Collect service health without changing state
Query only the services you deliberately listed. The default workflow is observation, not automatic remediation, so an unexpected stopped service remains visible for review instead of being silently restarted.
Configuration or code example: review values for your environment
$ServiceEvidence = foreach ($Name in $ServiceNames) { try { $Service = Get-Service -Name $Name -ErrorAction Stop; [pscustomobject]@{ Name=$Service.Name; DisplayName=$Service.DisplayName; Status=[string]$Service.Status; Found=$true } } catch { [pscustomobject]@{ Name=$Name; DisplayName=$null; Status='NotFound'; Found=$false } } } - Write a durable daily status record
Create the evidence directory, combine backup and service status with a collection timestamp, and write both human-readable and structured outputs. Keep reporting local first; integrate mail, webhooks, or monitoring only through an organization-supported path after the evidence is trustworthy.
Changes system state: review before running
New-Item -ItemType Directory -Path $ReportPath -Force | Out-Null $Stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $Summary = [pscustomobject]@{ CollectedAt=(Get-Date).ToString('o'); BackupStatus=$BackupEvidence.Status; BackupFile=$BackupEvidence.File; BackupAgeHours=$BackupEvidence.AgeHours; UnhealthyServices=@($ServiceEvidence | Where-Object Status -ne 'Running').Count } $Summary | ConvertTo-Json -Depth 4 | Set-Content -Path (Join-Path $ReportPath "status-$Stamp.json") -Encoding UTF8 $ServiceEvidence | Export-Csv -Path (Join-Path $ReportPath "services-$Stamp.csv") -NoTypeInformation -Encoding UTF8 $Summary | Format-List | Out-String | Set-Content -Path (Join-Path $ReportPath "status-$Stamp.txt") -Encoding UTF8 - Test the script interactively before scheduling it
Save the working code as C:\Scripts\HomeLabStatus.ps1, run it in the same security context you intend to schedule, and inspect the newest outputs. For a UNC backup path, verify that the chosen account can reach the share without depending on an interactive drive mapping.
Review before running: verify target, scope, and execution context
powershell.exe -NoProfile -File C:\Scripts\HomeLabStatus.ps1 Get-ChildItem C:\OpsEvidence\HomeLabStatus | Sort-Object LastWriteTime -Descending | Select-Object -First 6 Name, LastWriteTime, Length
- Create one daily scheduled task
Schedule the single status script rather than three loosely coupled tasks. The account that creates/runs the task must have the permissions required by the backup path, services, script, and report directory. Do not embed a password in source control or documentation.
Changes system state: review before running
schtasks /create /tn "OpsStack\HomeLabStatus" /tr "powershell.exe -NoProfile -File C:\Scripts\HomeLabStatus.ps1" /sc daily /st 09:00 /f schtasks /query /tn "OpsStack\HomeLabStatus" /v /fo list
- Run the scheduled task on demand and validate its context
Use the saved task definition for an immediate test. schtasks /run uses the program and account already stored in the task, so this test is useful for catching differences between an interactive shell and the scheduled execution context.
Changes system state: review before running
schtasks /run /tn "OpsStack\HomeLabStatus" Start-Sleep -Seconds 5 schtasks /query /tn "OpsStack\HomeLabStatus" /v /fo list Get-ChildItem C:\OpsEvidence\HomeLabStatus | Sort-Object LastWriteTime -Descending | Select-Object -First 6 Name, LastWriteTime, Length
- Keep remediation separate from monitoring
If a stopped service is confirmed to be one that should be running, remediate it explicitly after reviewing dependencies and the failure context. Do not add blanket automatic Start-Service behavior to the status collector unless you have defined which services are safe to restart and how restart failures are escalated.
Changes system state: review before running
Get-Service -Name '<SERVICE_NAME>' | Select-Object Name, Status, StartType Start-Service -Name '<SERVICE_NAME>'
- Add notifications only through a supported path
If daily or exception notifications are needed, route the structured evidence into a supported monitoring, webhook, ticketing, or mail mechanism. Microsoft marks Send-MailMessage obsolete and recommends against using it for new automation because it does not guarantee secure SMTP connections.
Validation
Force a stale-data test by setting a threshold below the age of the newest test artifact and confirm the report changes to Stale without deleting or modifying the backup file.
Use a deliberately nonexistent service name in the test list and confirm the report records NotFound rather than terminating the whole collection.
Run the scheduled task on demand and confirm it creates new output files under the scheduled task's saved execution context.
Review before running: verify target, scope, and execution context
schtasks /run /tn "OpsStack\HomeLabStatus" schtasks /query /tn "OpsStack\HomeLabStatus" /v /fo list
Open the newest JSON and CSV files and reconcile the backup state and service counts with a manual check of the source path and Get-Service output.
Document separately how restore verification is performed for the protected workload; a Fresh file result is not a restore test.
Troubleshooting
If the interactive run succeeds but the scheduled run fails, compare the task account, working paths, share permissions, and whether the script depends on an interactive drive mapping.
If the backup path is empty unexpectedly, verify the filter and path independently before increasing the freshness threshold.
If a service query returns AccessDenied or NotFound, validate the exact service name and execution context before adding broader privileges.
If schtasks reports a task error, query the task verbosely and inspect Task Scheduler operational history before recreating it.
Read-only command: verify target and scope
schtasks /query /tn "OpsStack\HomeLabStatus" /v /fo list
Cleanup or Rollback
Remove the scheduled task if the Lab is no longer needed.
Warning: Deleting the task stops future collection; preserve any evidence files you still need first.
Destructive: review before running
schtasks /delete /tn "OpsStack\HomeLabStatus" /f
Remove only the Lab's generated evidence directory after deciding that its history is no longer required.
Warning: This permanently deletes the generated status history at that path.
Destructive: review before running
Remove-Item -Path 'C:\OpsEvidence\HomeLabStatus' -Recurse -Force
Next Improvements
Send only exception conditions to an organization-approved notification path rather than mailing every successful run.
Add retention for old status files after deciding how much evidence history is useful.
Add a real restore drill for the backup platform so freshness evidence and recoverability evidence are tracked separately.
Move thresholds and monitored service names into a small configuration file once the script behavior is stable.
