Introduction to Scheduled Task-Based Backup Scripts on Windows Server 2019

While enterprise backup solutions like Veeam or Azure Backup provide comprehensive protection, many organizations also use custom PowerShell and batch scripts for targeted, supplemental backups — backing up specific application data, configuration files, database exports, or IIS content on a schedule. Windows Task Scheduler is the native Windows Server 2019 mechanism for running these scripts automatically. Combined with robust error handling, logging, and notification capabilities in PowerShell, script-based backup jobs can be highly reliable and easier to troubleshoot than black-box backup agents. This guide covers designing resilient backup scripts with logging and alerting, registering them with Task Scheduler, and implementing best practices for script-based backup automation.

Designing a PowerShell Backup Script with Logging

A well-structured backup script should: define source and destination paths as variables at the top, create a timestamped log file for each run, implement try/catch error handling, and send an email notification on failure. The following example backs up a web application’s data directory to a network share with full logging:

# backup-webapp.ps1
param(
    [string]$SourcePath = "C:inetpubwwwrootmyappdata",
    [string]$DestinationBase = "\backupserverbackupswebapp",
    [string]$SmtpServer = "smtp.company.com",
    [string]$AlertTo = "[email protected]",
    [string]$AlertFrom = "[email protected]"
)

$DateStamp = Get-Date -Format "yyyyMMdd_HHmmss"
$LogFile = "C:BackupLogswebapp-backup-$DateStamp.log"
$BackupDest = Join-Path $DestinationBase $DateStamp

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
}

function Send-AlertEmail {
    param([string]$Subject, [string]$Body)
    try {
        Send-MailMessage -SmtpServer $SmtpServer -From $AlertFrom -To $AlertTo -Subject $Subject -Body $Body
    } catch {
        Write-Log "Failed to send alert email: $($_.Exception.Message)" -Level "ERROR"
    }
}

# Ensure log directory exists
New-Item -ItemType Directory -Path "C:BackupLogs" -Force | Out-Null

Write-Log "Starting backup of $SourcePath to $BackupDest"

try {
    if (-not (Test-Path $SourcePath)) {
        throw "Source path $SourcePath does not exist."
    }
    New-Item -ItemType Directory -Path $BackupDest -Force | Out-Null
    robocopy $SourcePath $BackupDest /E /COPYALL /R:3 /W:10 /LOG+:$LogFile /NP /TEE
    $robocopyExit = $LASTEXITCODE
    if ($robocopyExit -ge 8) {
        throw "Robocopy failed with exit code $robocopyExit. Check log for details."
    }
    Write-Log "Backup completed successfully. Robocopy exit code: $robocopyExit"
} catch {
    Write-Log "BACKUP FAILED: $($_.Exception.Message)" -Level "ERROR"
    Send-AlertEmail -Subject "BACKUP FAILED: webapp on $env:COMPUTERNAME" -Body "Backup script failed at $(Get-Date). Error: $($_.Exception.Message). Log: $LogFile"
    exit 1
}

# Cleanup old backups older than 30 days
Get-ChildItem -Path $DestinationBase -Directory | Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-30) } | Remove-Item -Recurse -Force
Write-Log "Cleanup complete. Removed backups older than 30 days."

Using Robocopy for Reliable File Backup

Robocopy (Robust File Copy) is the recommended tool for scripted file backup on Windows Server 2019. Key switches for backup operations:

/E — copy all subdirectories including empty ones. /COPYALL — copy all file attributes including data, attributes, timestamps, ACLs (security), owner, and auditing. /B — copy in backup mode (requires SeBackupPrivilege, bypasses file permission restrictions). /R:3 — retry 3 times on failed copies. /W:10 — wait 10 seconds between retries. /MIR — mirror the destination to the source (deletes files from destination that no longer exist in source). /LOG+:logfile — append to a log file. /NP — no progress percentage in output. /TEE — output to console AND log file simultaneously.

robocopy "C:inetpubwwwrootmyapp" "\backupserverbackupswebapp20260517_220000" /E /COPYALL /B /R:3 /W:10 /LOG+:"C:BackupLogsrobocopy.log" /NP /TEE

Robocopy exit codes: 0 = no files copied (nothing changed), 1 = files copied successfully, 2 = extra files in destination, 4 = mismatched files, 8 = some files not copied (errors), 16 = fatal error. Exit codes 0–7 are considered success; 8 and above indicate errors requiring investigation.

Registering the Backup Script with Task Scheduler

Register the backup script as a scheduled task running under the SYSTEM account (or a dedicated service account with access to the backup destination) via PowerShell:

$scriptPath = "C:BackupScriptsbackup-webapp.ps1"
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NonInteractive -NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger -Daily -At "23:00"
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 4) -StartWhenAvailable -Priority 4 -MultipleInstances IgnoreNew
$principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITYSYSTEM" -RunLevel Highest -LogonType ServiceAccount

Register-ScheduledTask -TaskName "Backup-WebApp-Nightly" -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Description "Nightly backup of webapp data to network share"

The -StartWhenAvailable flag ensures the task runs as soon as possible if it was missed (e.g., if the server was down at the scheduled time). -MultipleInstances IgnoreNew prevents a second instance from starting if the previous run is still in progress.

Rotating and Cleaning Up Old Backups

Script-based backups create new folders or files with each run. Without cleanup, backup storage fills up. Implement retention cleanup within the backup script or as a separate cleanup task. To keep the last 14 daily backups and delete older ones:

$BackupRoot = "\backupserverbackupswebapp"
$RetainCount = 14
$AllBackups = Get-ChildItem -Path $BackupRoot -Directory | Sort-Object CreationTime -Descending
if ($AllBackups.Count -gt $RetainCount) {
    $BackupsToDelete = $AllBackups | Select-Object -Skip $RetainCount
    foreach ($backup in $BackupsToDelete) {
        Remove-Item -Path $backup.FullName -Recurse -Force
        Write-Log "Deleted old backup: $($backup.FullName)"
    }
}

Monitoring Task Scheduler Execution History

Check Task Scheduler execution history for the backup task from PowerShell:

$task = Get-ScheduledTask -TaskName "Backup-WebApp-Nightly"
$task | Get-ScheduledTaskInfo

This shows the last run time, last result (0 = success), next run time, and number of missed runs. Query the Task Scheduler event log for historical runs:

Get-WinEvent -LogName "Microsoft-Windows-TaskScheduler/Operational" | Where-Object { $_.Message -like "*Backup-WebApp-Nightly*" } | Select-Object TimeCreated, Id, Message | Sort-Object TimeCreated -Descending | Select-Object -First 20 | Format-List

Event ID 102 indicates a task completed successfully. Event ID 101 indicates a task failed to start. Event ID 103 indicates a task completed with an error (non-zero exit code). Use these events as the basis for monitoring alerts in your monitoring system.