How to Configure Scheduled Task-Based Backup Scripts on Windows Server 2025
Windows Server 2025 ships with powerful built-in scheduling and scripting capabilities, but many backup requirements exceed what the Windows Server Backup GUI exposes. Custom PowerShell backup scripts give you precise control over what gets backed up, where it goes, how it is archived, and who gets notified when something goes wrong. By pairing a well-structured script with the Windows Task Scheduler, you can build a fully automated backup system that handles file server replication with Robocopy, SQL Server database exports via Invoke-Sqlcmd, compressed archives with Compress-Archive, and email notifications — all with detailed logging. This guide covers writing robust backup scripts, registering them as scheduled tasks under a service account, and monitoring job history.
Prerequisites
- Windows Server 2025 with PowerShell 7.4 or later (or Windows PowerShell 5.1)
- Administrator rights or a dedicated backup service account with appropriate permissions
- SMTP relay accessible from the server (for email notifications)
- SQL Server Management Objects (SMO) or SQLPS module if backing up SQL Server
- Destination backup path: local volume, DFS namespace, or UNC share with write access for the service account
- Robocopy available (included in Windows Server 2025 by default)
Step 1: Create the Backup Service Account
Running backup scripts as SYSTEM grants excessive privileges. Create a dedicated service account with only the rights it needs:
# Create a managed service account or standard account
New-ADUser -Name "svc-backup" `
-SamAccountName "svc-backup" `
-UserPrincipalName "[email protected]" `
-Path "OU=ServiceAccounts,DC=contoso,DC=local" `
-AccountPassword (ConvertTo-SecureString "S3cur3B@ckupPwd!" -AsPlainText -Force) `
-PasswordNeverExpires $true `
-Enabled $true
# Grant the account "Log on as a batch job" right via Group Policy or local policy
# Grant read access to source shares and write access to backup destination
Step 2: Write a Robust File Server Backup Script with Robocopy
Save the following script as C:ScriptsBackup-FileServer.ps1. It uses Start-Transcript for full session logging, Robocopy with mirroring, and structured error handling:
#Requires -Version 5.1
param(
[Parameter(Mandatory)][string]$SourcePath,
[Parameter(Mandatory)][string]$DestinationPath,
[string]$SmtpServer = "smtp.contoso.local",
[string]$NotifyEmail = "[email protected]",
[string]$FromEmail = "[email protected]"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# --- Logging setup ----------------------------------------------------------
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$LogDir = "C:LogsBackup"
$TranscriptFile = "$LogDirFileServer_$Timestamp.log"
$RobocopyLog = "$LogDirRobocopy_$Timestamp.log"
if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir | Out-Null }
Start-Transcript -Path $TranscriptFile -Append
$JobStatus = "SUCCESS"
$ErrorDetails = ""
try {
Write-Output "[$(Get-Date -Format 'HH:mm:ss')] Starting file server backup"
Write-Output " Source : $SourcePath"
Write-Output " Destination : $DestinationPath"
# Validate source
if (-not (Test-Path $SourcePath)) {
throw "Source path not accessible: $SourcePath"
}
# Robocopy with mirroring (/MIR), copy all attributes (/COPYALL),
# 4 threads (/MT:4), retry 2 times (/R:2 /W:5), exclude temp files
$RoboArgs = @(
$SourcePath, $DestinationPath,
"/MIR", "/COPYALL", "/MT:4",
"/R:2", "/W:5",
"/XF", "*.tmp", "~*", "thumbs.db",
"/XD", "`$RECYCLE.BIN", "System Volume Information",
"/LOG+:$RobocopyLog",
"/TEE", "/NP", "/NDL"
)
$RoboProcess = Start-Process -FilePath "robocopy.exe" `
-ArgumentList $RoboArgs `
-Wait -PassThru -NoNewWindow
# Robocopy exit codes: 0-3 = success/warnings, 8+ = errors
if ($RoboProcess.ExitCode -ge 8) {
throw "Robocopy failed with exit code $($RoboProcess.ExitCode). See $RobocopyLog"
}
Write-Output "[$(Get-Date -Format 'HH:mm:ss')] Robocopy completed (exit code: $($RoboProcess.ExitCode))"
# --- Create daily compressed archive ------------------------------------
$ArchiveDir = "$DestinationPath_Archives"
$ArchivePath = "$ArchiveDirFileServer_$Timestamp.zip"
if (-not (Test-Path $ArchiveDir)) { New-Item -ItemType Directory -Path $ArchiveDir | Out-Null }
Write-Output "[$(Get-Date -Format 'HH:mm:ss')] Creating compressed archive: $ArchivePath"
Compress-Archive -Path "$DestinationPath*" -DestinationPath $ArchivePath -CompressionLevel Optimal -Force
# Purge archives older than 30 days
Get-ChildItem -Path $ArchiveDir -Filter "*.zip" |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
Remove-Item -Force
Write-Output "[$(Get-Date -Format 'HH:mm:ss')] Archive created and old archives purged"
} catch {
$JobStatus = "FAILED"
$ErrorDetails = $_.Exception.Message
Write-Error "Backup failed: $ErrorDetails"
} finally {
Stop-Transcript
# --- Email notification --------------------------------------------------
$Subject = "[$JobStatus] File Server Backup - $(Get-Date -Format 'yyyy-MM-dd')"
$Body = @"
Backup Job Result: $JobStatus
Server : $env:COMPUTERNAME
Source : $SourcePath
Destination: $DestinationPath
Completed : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
$(if ($ErrorDetails) { "ERROR: $ErrorDetails" })
See full log: $TranscriptFile
"@
try {
Send-MailMessage -SmtpServer $SmtpServer `
-From $FromEmail -To $NotifyEmail `
-Subject $Subject -Body $Body
} catch {
Write-Warning "Email notification failed: $_"
}
if ($JobStatus -eq "FAILED") { exit 1 }
}
Step 3: Write a SQL Server Backup Script
Save the following as C:ScriptsBackup-SqlServer.ps1. It uses Invoke-Sqlcmd to run native T-SQL backup commands:
#Requires -Version 5.1
param(
[string]$SqlInstance = "localhost",
[string]$BackupRoot = "D:SQLBackups",
[string]$SmtpServer = "smtp.contoso.local",
[string]$NotifyEmail = "[email protected]"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$LogPath = "C:LogsBackupSQL_$Timestamp.log"
Start-Transcript -Path $LogPath
$Results = @()
try {
# Get all user databases (excluding system DBs)
$Databases = Invoke-Sqlcmd -ServerInstance $SqlInstance `
-Query "SELECT name FROM sys.databases WHERE database_id > 4 AND state_desc = 'ONLINE'" `
-TrustServerCertificate
foreach ($Db in $Databases) {
$DbName = $Db.name
$DbDir = "$BackupRoot$DbName"
$BackupFile = "$DbDir${DbName}_$Timestamp.bak"
if (-not (Test-Path $DbDir)) { New-Item -ItemType Directory -Path $DbDir | Out-Null }
Write-Output "Backing up database: $DbName -> $BackupFile"
try {
Invoke-Sqlcmd -ServerInstance $SqlInstance -TrustServerCertificate -Query @"
BACKUP DATABASE [$DbName]
TO DISK = N'$BackupFile'
WITH COMPRESSION, STATS = 10,
NAME = N'${DbName}-Full Backup $Timestamp',
CHECKSUM, CONTINUE_AFTER_ERROR;
"@
$Results += [PSCustomObject]@{ Database=$DbName; Status="OK"; File=$BackupFile }
# Purge backups older than 14 days for this database
Get-ChildItem -Path $DbDir -Filter "*.bak" |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-14) } |
Remove-Item -Force
} catch {
$Results += [PSCustomObject]@{ Database=$DbName; Status="FAILED"; File=$_.Exception.Message }
Write-Warning "Failed to back up $DbName`: $_"
}
}
} finally {
Stop-Transcript
$FailCount = ($Results | Where-Object { $_.Status -eq "FAILED" }).Count
$Subject = "$(if($FailCount){'[FAILED]'}else{'[OK]'}) SQL Backup - $(Get-Date -Format 'yyyy-MM-dd')"
$Body = $Results | Format-Table -AutoSize | Out-String
Send-MailMessage -SmtpServer $SmtpServer -From "[email protected]" `
-To $NotifyEmail -Subject $Subject -Body $Body
if ($FailCount -gt 0) { exit 1 }
}
Step 4: Register Scripts as Scheduled Tasks
Use Register-ScheduledTask to create properly configured tasks that run as the backup service account:
# --- Register the file server backup task -----------------------------------
$FSAction = New-ScheduledTaskAction `
-Execute "pwsh.exe" `
-Argument @"
-NonInteractive -ExecutionPolicy Bypass -File "C:ScriptsBackup-FileServer.ps1" -SourcePath "\fileserver01data" -DestinationPath "E:BackupsFileServer"
"@
$FSTrigger = New-ScheduledTaskTrigger -Daily -At "01:30AM"
$Principal = New-ScheduledTaskPrincipal `
-UserId "CONTOSOsvc-backup" `
-LogonType Password `
-RunLevel Highest
$Settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Hours 4) `
-RestartCount 1 `
-RestartInterval (New-TimeSpan -Minutes 10) `
-StartWhenAvailable
Register-ScheduledTask `
-TaskName "Backup - File Server Nightly" `
-TaskPath "CustomBackups" `
-Action $FSAction `
-Trigger $FSTrigger `
-Principal $Principal `
-Settings $Settings `
-Force
# Supply password for the service account
$Cred = Get-Credential -UserName "CONTOSOsvc-backup" -Message "Enter backup service account password"
Set-ScheduledTask -TaskName "Backup - File Server Nightly" `
-TaskPath "CustomBackups" `
-User "CONTOSOsvc-backup" `
-Password $Cred.GetNetworkCredential().Password
# --- Register the SQL backup task (runs at 03:00 AM) -----------------------
$SQLAction = New-ScheduledTaskAction `
-Execute "pwsh.exe" `
-Argument '-NonInteractive -ExecutionPolicy Bypass -File "C:ScriptsBackup-SqlServer.ps1" -SqlInstance "sql01.contoso.local" -BackupRoot "D:SQLBackups"'
$SQLTrigger = New-ScheduledTaskTrigger -Daily -At "03:00AM"
Register-ScheduledTask `
-TaskName "Backup - SQL Server Nightly" `
-TaskPath "CustomBackups" `
-Action $SQLAction `
-Trigger $SQLTrigger `
-Principal $Principal `
-Settings $Settings `
-Force
Step 5: Monitor Scheduled Task History and Last Run Results
Confirm tasks are running successfully and investigate failures through PowerShell:
# Enable task history if not already enabled
$TaskScheduler = New-Object -ComObject "Schedule.Service"
$TaskScheduler.Connect()
$TaskScheduler.GetFolder("").GetTask("") | Out-Null # force COM initialization
wevtutil set-log "Microsoft-Windows-TaskScheduler/Operational" /enabled:true /quiet
# List last run result for all custom backup tasks
Get-ScheduledTask -TaskPath "CustomBackups" | ForEach-Object {
$Info = $_ | Get-ScheduledTaskInfo
[PSCustomObject]@{
TaskName = $_.TaskName
LastRunTime = $Info.LastRunTime
LastTaskResult = $Info.LastTaskResult # 0 = success
NextRunTime = $Info.NextRunTime
}
} | Format-Table -AutoSize
# Pull task scheduler event log for failures
Get-WinEvent -LogName "Microsoft-Windows-TaskScheduler/Operational" |
Where-Object { $_.Id -in @(101, 103, 111) } | # failed / stopped with error
Select-Object TimeCreated, Id, Message -First 20 |
Format-List
Conclusion
Scheduled task-based backup scripts on Windows Server 2025 give you a flexible, auditable, and fully automated backup framework that adapts to virtually any workload. By writing parameterized PowerShell scripts with proper error handling, transcript logging, and email notification, you build a system where failures are immediately visible and logs provide clear diagnosis. Robocopy’s mirroring mode ensures your file server backups stay current while Invoke-Sqlcmd delivers native, compressed SQL Server backups with checksums. Wrapping everything in scheduled tasks registered under a least-privilege service account keeps the solution secure and maintainable. Review task history weekly and periodically test restoration from your backup archives to confirm that the data you are capturing is actually recoverable.