Introduction to Scheduled Task-Based Backup Scripts on Windows Server 2022
Windows Server 2022 ships with powerful built-in tools for automating backup operations: PowerShell for scripting, the Task Scheduler for timed execution, robocopy for file-level data copies, and wbadmin for system image and system state backups. Combining these into well-structured backup scripts — complete with logging, email notification, and validation — gives you a reliable, auditable backup solution without requiring third-party backup software. This guide walks through building production-ready backup scripts and scheduling them to run automatically.
Designing Your Backup Script Strategy
Before writing any code, determine what you need to back up and where the backups will go. A typical Windows Server 2022 backup strategy includes at minimum:
File-level backup: Using robocopy to mirror critical directories — application data, user profiles, web roots, database exports — to a backup share or local backup volume.
System state or image backup: Using wbadmin to capture the OS state including registry, boot files, and Active Directory data.
Database backup: For SQL Server, running T-SQL backup commands or using sqlcmd before the file-level copy.
Each backup type should have its own script that logs results, reports failures, and rotates old backup sets to prevent disk exhaustion. Scripts should be stored in a secured location such as C:ScriptsBackup with permissions restricted to Administrators and the service account that runs the tasks.
Writing a Robocopy-Based File Backup Script
Robocopy (Robust File Copy) is the standard file synchronization tool on Windows Server. It supports restartable copies, network resilience, detailed logging, and mirror mode which removes files from the destination that no longer exist at the source. The following script backs up multiple source directories to a timestamped folder on a network share:
#Requires -RunAsAdministrator
# FileBackup.ps1 - Robocopy-based file backup with logging
param(
[string]$BackupRoot = "\fileserverbackupsWS2022-APP01",
[string]$LogDir = "C:LogsBackup"
)
$ErrorActionPreference = "Stop"
$Date = Get-Date -Format "yyyy-MM-dd"
$Timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
$LogFile = "$LogDirfilebkp_$Timestamp.log"
$ExitCode = 0
# Create log directory if it doesn't exist
if (-not (Test-Path $LogDir)) {
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
}
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$Entry = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
Add-Content -Path $LogFile -Value $Entry
Write-Host $Entry
}
# Source directories to back up
$Sources = @(
@{ Src = "C:inetpubwwwroot"; Dst = "WebRoot" },
@{ Src = "C:AppDataConfig"; Dst = "AppConfig" },
@{ Src = "C:UsersSvcAccounts"; Dst = "SvcProfiles" }
)
$BackupSet = "$BackupRoot$Date"
Write-Log "Starting file backup to $BackupSet"
foreach ($Item in $Sources) {
$Dest = "$BackupSet$($Item.Dst)"
Write-Log "Backing up $($Item.Src) -> $Dest"
$RoboArgs = @(
$Item.Src,
$Dest,
"/MIR", # Mirror mode
"/R:3", # Retry 3 times
"/W:10", # Wait 10 seconds between retries
"/NP", # No progress percentage
"/NFL", # No file list (cleaner logs)
"/NDL", # No directory list
"/LOG+:$LogFile"
)
$Result = & robocopy @RoboArgs
$RC = $LASTEXITCODE
# Robocopy exit codes: 0=no change, 1=copied OK, 2-7=warnings, 8+=error
if ($RC -ge 8) {
Write-Log "Robocopy FAILED for $($Item.Src) with exit code $RC" -Level "ERROR"
$ExitCode = 1
} else {
Write-Log "Robocopy completed for $($Item.Src), exit code: $RC"
}
}
Write-Log "File backup finished. Overall status: $(if ($ExitCode -eq 0) {'SUCCESS'} else {'FAILURE'})"
exit $ExitCode
Writing a wbadmin-Based System Backup Script
The following script runs a wbadmin system state backup and logs the result. System state backups require a local volume target:
#Requires -RunAsAdministrator
# SystemStateBackup.ps1 - wbadmin system state backup with logging
param(
[string]$BackupTarget = "E:",
[string]$LogDir = "C:LogsBackup"
)
$ErrorActionPreference = "Continue"
$Timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
$LogFile = "$LogDirssbkp_$Timestamp.log"
$ExitCode = 0
if (-not (Test-Path $LogDir)) {
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
}
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$Entry = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
Add-Content -Path $LogFile -Value $Entry
Write-Host $Entry
}
Write-Log "Starting system state backup to $BackupTarget"
# Run wbadmin and capture output
$WbOutput = & wbadmin start systemstatebackup -backupTarget:$BackupTarget -quiet 2>&1
$WbExit = $LASTEXITCODE
foreach ($Line in $WbOutput) {
Write-Log "wbadmin: $Line"
}
if ($WbExit -ne 0) {
Write-Log "System state backup FAILED (exit code $WbExit)" -Level "ERROR"
$ExitCode = 1
} else {
Write-Log "System state backup completed successfully"
}
Write-Log "Backup script finished with exit code $ExitCode"
exit $ExitCode
Logging Backup Results to the Windows Event Log
Writing backup results to the Windows Event Log makes them visible in Event Viewer and allows monitoring tools to alert on failures. First, create a custom event log source (run once as Administrator):
New-EventLog -LogName Application -Source "BackupScript"
Then add the following function to your backup scripts:
function Write-BackupEvent {
param(
[string]$Message,
[string]$EntryType = "Information", # Information, Warning, Error
[int]$EventId = 1000
)
Write-EventLog -LogName Application -Source "BackupScript" `
-EntryType $EntryType -EventId $EventId -Message $Message
}
# Usage examples:
Write-BackupEvent -Message "File backup completed successfully" -EventId 1001
Write-BackupEvent -Message "System state backup FAILED: robocopy exit code 16" `
-EntryType "Error" -EventId 1002
Query backup events from the event log to verify they are being written:
Get-EventLog -LogName Application -Source "BackupScript" -Newest 20 |
Select-Object TimeGenerated, EntryType, EventID, Message
Email Notification on Backup Failure
Email alerts ensure administrators are notified immediately when a backup fails. The following function uses PowerShell’s Send-MailMessage to send failure notifications via an internal SMTP relay:
function Send-BackupAlert {
param(
[string]$Subject,
[string]$Body,
[string]$SmtpServer = "smtp.contoso.com",
[string]$From = "[email protected]",
[string[]]$To = @("[email protected]","[email protected]")
)
try {
Send-MailMessage -SmtpServer $SmtpServer -From $From -To $To `
-Subject $Subject -Body $Body -BodyAsHtml $false
} catch {
Write-Log "Failed to send email alert: $_" -Level "WARNING"
}
}
# In your backup script, call this when a failure is detected:
if ($ExitCode -ne 0) {
$Server = $env:COMPUTERNAME
Send-BackupAlert `
-Subject "BACKUP FAILURE: $Server $(Get-Date -Format 'yyyy-MM-dd')" `
-Body "The scheduled backup on $Server failed at $(Get-Date). Check the log at $LogFile for details."
}
If your environment uses SMTP with authentication or TLS, add the appropriate parameters:
$Cred = Get-Credential # Or load from encrypted credential store
Send-MailMessage -SmtpServer "smtp.office365.com" -Port 587 -UseSsl `
-Credential $Cred -From $From -To $To -Subject $Subject -Body $Body
Backing Up to a Network Share with Credentials
Robocopy itself does not handle authentication to network shares — the share must be accessible from the running context. When running as SYSTEM (the typical context for scheduled tasks), network shares require a persistent drive mapping or the use of a service account with share access. The recommended approach is to run the task under a dedicated backup service account:
# Mount the network share with credentials before running robocopy
# Run this in the backup script if credentials are needed:
$SecurePass = ConvertTo-SecureString "ServiceAccountPass" -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential("DOMAINBackupSvc", $SecurePass)
# Map the share as a PSDrive
New-PSDrive -Name "BKP" -PSProvider FileSystem `
-Root "\fileserverbackups" -Credential $Cred -Persist
# Use it in robocopy
robocopy C:AppDataConfig "BKP:WS2022-APP01Config" /MIR /R:3 /W:10 /LOG+:$LogFile
# Remove the drive mapping when done
Remove-PSDrive -Name "BKP" -Force
For production systems, avoid storing plaintext passwords in scripts. Instead, use Windows Data Protection API (DPAPI) to encrypt the credential for the service account:
# Encrypt and save (run once as the service account):
$Cred = Get-Credential
$Cred.Password | ConvertFrom-SecureString | Out-File "C:ScriptsBackupbkpsvc.cred"
# Load in the backup script:
$SecurePass = Get-Content "C:ScriptsBackupbkpsvc.cred" | ConvertTo-SecureString
$Cred = New-Object System.Management.Automation.PSCredential("DOMAINBackupSvc", $SecurePass)
Creating Scheduled Tasks for Backup Scripts
Use Register-ScheduledTask to create scheduled tasks that run the backup scripts automatically. The following creates tasks for both the file backup and system state backup scripts:
# File backup - runs at 11:00 PM daily
$FileBackupAction = New-ScheduledTaskAction `
-Execute "powershell.exe" `
-Argument "-NonInteractive -ExecutionPolicy Bypass -File C:ScriptsBackupFileBackup.ps1"
$FileBackupTrigger = New-ScheduledTaskTrigger -Daily -At "11:00PM"
$Principal = New-ScheduledTaskPrincipal `
-UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$Settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Hours 3) `
-MultipleInstances IgnoreNew `
-StartWhenAvailable $true
Register-ScheduledTask -TaskName "FileBackup-Daily" `
-Action $FileBackupAction -Trigger $FileBackupTrigger `
-Principal $Principal -Settings $Settings `
-Description "Daily file backup via robocopy"
# System state backup - runs at 2:00 AM daily
$SSBackupAction = New-ScheduledTaskAction `
-Execute "powershell.exe" `
-Argument "-NonInteractive -ExecutionPolicy Bypass -File C:ScriptsBackupSystemStateBackup.ps1"
$SSBackupTrigger = New-ScheduledTaskTrigger -Daily -At "02:00AM"
Register-ScheduledTask -TaskName "SystemStateBackup-Daily" `
-Action $SSBackupAction -Trigger $SSBackupTrigger `
-Principal $Principal -Settings $Settings `
-Description "Daily system state backup"
Verify the tasks are registered:
Get-ScheduledTask | Where-Object { $_.TaskName -like "*Backup*" } |
Select-Object TaskName, State, @{N='NextRun';E={($_ | Get-ScheduledTaskInfo).NextRunTime}}
Backup Script Error Handling
Production backup scripts must handle errors gracefully and continue where possible. Use try/catch blocks around critical operations:
$OverallSuccess = $true
foreach ($Item in $Sources) {
try {
$Result = & robocopy $Item.Src $Item.Dst /MIR /R:3 /W:10 /NP
if ($LASTEXITCODE -ge 8) {
throw "Robocopy failed with exit code $LASTEXITCODE"
}
Write-Log "Backed up $($Item.Src) successfully"
} catch {
Write-Log "ERROR backing up $($Item.Src): $_" -Level "ERROR"
Write-BackupEvent -Message "Backup failed for $($Item.Src): $_" `
-EntryType "Error" -EventId 1002
$OverallSuccess = $false
# Continue to next source instead of aborting entire script
}
}
if (-not $OverallSuccess) {
Send-BackupAlert -Subject "Partial backup failure on $env:COMPUTERNAME" `
-Body "One or more backup sources failed. Review the log at $LogFile"
exit 1
}
exit 0
Backup Validation with Hash Comparison
Verifying that backed-up files match their originals using cryptographic hashes ensures backup integrity. The following function computes and compares SHA-256 hashes for critical files after backup:
function Compare-BackupIntegrity {
param(
[string]$SourceDir,
[string]$BackupDir,
[int]$SampleSize = 50
)
$SourceFiles = Get-ChildItem -Path $SourceDir -Recurse -File |
Get-Random -Count $SampleSize
$Failures = 0
foreach ($File in $SourceFiles) {
$RelPath = $File.FullName.Substring($SourceDir.Length)
$BackupFile = Join-Path $BackupDir $RelPath
if (-not (Test-Path $BackupFile)) {
Write-Log "MISSING in backup: $RelPath" -Level "ERROR"
$Failures++
continue
}
$SrcHash = (Get-FileHash -Path $File.FullName -Algorithm SHA256).Hash
$DstHash = (Get-FileHash -Path $BackupFile -Algorithm SHA256).Hash
if ($SrcHash -ne $DstHash) {
Write-Log "HASH MISMATCH: $RelPath" -Level "ERROR"
$Failures++
}
}
Write-Log "Integrity check: $($SourceFiles.Count) files sampled, $Failures failures"
return $Failures
}
Rotating Backup Sets
To prevent backup storage from filling up, implement retention-based rotation. The following removes backup folders older than a specified number of days:
function Remove-OldBackups {
param(
[string]$BackupRoot,
[int]$RetainDays = 14
)
$CutoffDate = (Get-Date).AddDays(-$RetainDays)
$OldSets = Get-ChildItem -Path $BackupRoot -Directory |
Where-Object { $_.CreationTime -lt $CutoffDate }
foreach ($Set in $OldSets) {
Write-Log "Removing old backup set: $($Set.FullName) (created $($Set.CreationTime))"
try {
Remove-Item -Path $Set.FullName -Recurse -Force
Write-Log "Removed $($Set.FullName)"
} catch {
Write-Log "Failed to remove $($Set.FullName): $_" -Level "WARNING"
}
}
Write-Log "Rotation complete. Removed $($OldSets.Count) backup set(s)"
}
Call this at the end of each backup script after validating the current backup completed successfully:
if ($ExitCode -eq 0) {
Remove-OldBackups -BackupRoot "\fileserverbackupsWS2022-APP01" -RetainDays 14
}
Testing Backup Scripts
Before enabling scheduled execution, test each script manually using PowerShell with elevated privileges:
# Test the file backup script
& "C:ScriptsBackupFileBackup.ps1" -Verbose
# Check the last exit code
echo "Exit code: $LASTEXITCODE"
# Run a scheduled task immediately (for testing)
Start-ScheduledTask -TaskName "FileBackup-Daily"
# Check the task result after a few minutes
Get-ScheduledTaskInfo -TaskName "FileBackup-Daily" |
Select-Object LastRunTime, LastTaskResult, NextRunTime
A LastTaskResult of 0 indicates success. Any non-zero value indicates the script exited with an error. Check the log file and Event Viewer for details when troubleshooting failures.
Conclusion
Building scheduled backup scripts on Windows Server 2022 using PowerShell, robocopy, and wbadmin gives you a flexible, auditable backup solution. By integrating Event Log writing, email alerts, hash-based integrity validation, and automated rotation, you create a backup system that not only protects data but also actively reports on its own health. Test scripts manually before scheduling them, verify task execution via Event Viewer, and review backup logs weekly to catch any silent failures before they become a disaster.