Automation and Task Scheduling
Reviewed & published by Brayan K
Turn your one-off commands into real, reusable .ps1 scripts that handle errors gracefully and run themselves on a schedule — exactly how working sysadmins automate the boring stuff.
Part of the free PowerShell course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Write and run a reusable .ps1 script with param() inputs
- Allow scripts safely with Set-ExecutionPolicy RemoteSigned
- Handle failures with try/catch/finally and -ErrorAction Stop
- Install and import modules from the PowerShell Gallery
- Schedule scripts with Register-ScheduledTask (cron on Linux/macOS)
- Build a full automation: clean up old log files on a timer
1. From Commands to a Script File
A script is simply the commands you've been typing, saved into a file ending in .ps1. You run it by name. Adding a param() block at the very top turns it from a one-trick file into a reusable tool: the caller passes values in, so the same script works for many situations. Read this, save it as save-me.ps1, and run it.
# ===== save-me.ps1 — your first real PowerShell SCRIPT =====
# A script is just commands saved in a file ending in .ps1.
# Run it by name from a terminal: .\save-me.ps1
# param() MUST be the first code in the file. It declares inputs the
# caller can pass, so the script is reusable instead of hard-coded.
param(
[string]$Name = 'world' # a parameter with a default value
)
Write-Output "Hello, $Name!" # "$Name" is replaced by the value
Write-Output "Today is $(Get-Date -Format 'yyyy-MM-dd')"2. The Execution Policy
The first time you try to run a script, Windows often refuses with "running scripts is disabled on this system." That's the execution policy — a safety gate so a downloaded file can't run code behind your back. The fix is to set it to RemoteSigned: your own local scripts run freely, but anything downloaded from the internet must be digitally signed first.
3. Handling Errors with try/catch/finally
A scheduled script runs with no human watching, so it must cope with failure. Wrap risky work in try { … }; if it throws, the matching catch { … } runs your recovery code; finally { … } always runs (great for cleanup). The catch is the gotcha: most cmdlet errors don't throw by default — they just print red text and the script keeps going. Add -ErrorAction Stop (or set $ErrorActionPreference = 'Stop') to make them throw so catch can actually fire.
Your turn. Fill in the two blanks marked ___ so this script catches a missing file instead of crashing, then run it.
# 🎯 YOUR TURN — make this script survive a missing file.
# By default a non-terminating error is just printed and the script
# carries on. -ErrorAction Stop turns it into a real exception that
# try/catch can actually catch.
$path = 'C:\Reports\sales.csv'
try {
# 1) Read the file, but force a STOP on failure so catch fires
$data = Get-Content $path ___ # 👉 add: -ErrorAction Stop
Write-Output "Read $($data.Count) lines."
}
catch {
# 2) $_ is the error that was thrown — log its message
Write-Warning "Could not read the file: $(___)" # 👉 use $_.Exception.Message
}
finally {
# finally ALWAYS runs — success or failure — great for cleanup
Write-Output 'Done.'
}
# ✅ Expected output (when C:\Reports\sales.csv does NOT exist):
# WARNING: Could not read the file: Cannot find path 'C:\Reports\sales.csv' because it does not exist.
# Done.4. Modules — Reusing Other People's Code
A module is a packaged set of cmdlets. Install-Module downloads one from the PowerShell Gallery (once per machine); Import-Module loads it so you can use its commands in the current session. This is how you get powerful tools — like PSScriptAnalyzer, which lints your scripts for mistakes — without writing them yourself.
# A MODULE is a reusable package of cmdlets someone else (or you) wrote.
# Modules are how you get extra power without reinventing it.
# 1) What's already loaded in this session?
Get-Module
# (built-ins like Microsoft.PowerShell.Management appear here)
# 2) Find and INSTALL a module from the PowerShell Gallery (once per machine).
# -Scope CurrentUser avoids needing Administrator.
Install-Module -Name PSScriptAnalyzer -Scope CurrentUser
# 3) IMPORT it to use its cmdlets in THIS session.
# (Modern PowerShell auto-imports on first use, but be explicit in scripts.)
Import-Module PSScriptAnalyzer
# 4) See what the module gave you
Get-Command -Module PSScriptAnalyzer
# Invoke-ScriptAnalyzer, Invoke-Formatter, ...
# 5) Use it — lint a script for common mistakes
Invoke-ScriptAnalyzer -Path .\save-me.ps15. Scheduling a Script to Run Itself
The payoff: tell Windows to run your script on a timer. A scheduled task is built from three parts — an Action (what to run), a Trigger (when), and Settings (how) — then registered with Register-ScheduledTask. On Linux or macOS the same job is a one-line cron entry (0 2 * * * pwsh -File …); the script is identical, only the scheduler changes.
# Run a script automatically on a schedule — no human, no GUI.
# A task is built from three parts: an Action, a Trigger, and Settings.
# (On Linux/macOS the equivalent is a cron entry; this is the Windows way.)
# WHAT to run: launch PowerShell and execute our backup script
$action = New-ScheduledTaskAction `
-Execute 'pwsh.exe' `
-Argument '-NoProfile -File C:\Scripts\backup.ps1'
# WHEN to run: every day at 02:00
$trigger = New-ScheduledTaskTrigger -Daily -At '2:00AM'
# HOW to run: catch up if the PC was asleep at 2am
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable
# Register (create) the task in Windows Task Scheduler
Register-ScheduledTask `
-TaskName 'NightlyBackup' `
-Action $action -Trigger $trigger -Settings $settings `
-Description 'Backs up project files every night'
# Manage it later:
Get-ScheduledTask -TaskName 'NightlyBackup' # check it exists
Start-ScheduledTask -TaskName 'NightlyBackup' # run it now to test
Unregister-ScheduledTask -TaskName 'NightlyBackup' -Confirm:$false # deletePutting It Together: a Log-Cleanup Job
Here's a complete, schedulable script that uses everything from this lesson: param() inputs, $ErrorActionPreference = 'Stop', a pipeline to find old files, try/catch, and a -WhatIf dry-run switch so you can preview before deleting. Read it line by line — you understand every part now.
# ============================================================
# cleanup-logs.ps1 — delete log files older than N days,
# and write what it did to its own log. A real, schedulable job.
# ============================================================
param(
[string]$Folder = 'C:\Logs', # which folder to clean
[int] $OlderThan = 30, # delete files older than this many days
[switch]$WhatIf # dry-run: show, don't delete
)
$ErrorActionPreference = 'Stop' # make EVERY error terminating -> catchable
$cutoff = (Get-Date).AddDays(-$OlderThan)
$logFile = Join-Path $Folder 'cleanup.log'
$removed = 0
try {
# Find old *.log files (skip the cleanup log itself)
$old = Get-ChildItem -Path $Folder -Filter '*.log' -File |
Where-Object { $_.LastWriteTime -lt $cutoff -and $_.Name -ne 'cleanup.log' }
foreach ($file in $old) {
Remove-Item $file.FullName -WhatIf:$WhatIf # -WhatIf flows straight through
$removed++
}
$msg = "$(Get-Date -Format s) Removed $removed file(s) older than $OlderThan days."
Add-Content -Path $logFile -Value $msg # append a line to the log
Write-Output $msg
}
catch {
# Record the failure so the nightly run isn't a silent mystery
Add-Content -Path $logFile -Value "$(Get-Date -Format s) ERROR: $($_.Exception.Message)"
Write-Error $_
}Why -WhatIf? Anything destructive (Remove-Item, Copy-Item) supports it. Passing -WhatIf:$WhatIf flows your switch straight through, so one script does both a safe preview and the real run.
Pro Tips
- 💡 Always log scheduled runs to a file. A task has no visible console — Add-Content $logFile $msg is how you find out what happened at 2am.
- 💡 Test with -WhatIf before anything destructive. It shows what would happen without doing it.
- 💡 Put $ErrorActionPreference = 'Stop' at the top of scripts so a stray non-terminating error can't sail past your try/catch.
- 💡 Never hard-code paths. Take them as param() inputs (or use $env:TEMP, Join-Path) so the script works on any machine.
Common Errors (and the fix)
- "running scripts is disabled on this system" — the execution policy is blocking you. Run Set-ExecutionPolicy RemoteSigned -Scope CurrentUser, then try again.
- Your catch never runs and the script keeps going — the error was non-terminating. Add -ErrorAction Stop to the failing cmdlet, or set $ErrorActionPreference = 'Stop' at the top.
- "cannot find path … because it does not exist" — a hard-coded path is wrong on this machine. Pass paths as param() inputs and build them with Join-Path instead of typing C:\Logs directly.
- Scheduled task shows "Ready" but never seems to do anything — it has no console, so its output vanished. Log to a file from inside the script and check that file.
- "The term 'Invoke-ScriptAnalyzer' is not recognized" — the module isn't loaded. Run Install-Module PSScriptAnalyzer -Scope CurrentUser then Import-Module PSScriptAnalyzer.
📋 Quick Reference
| Task | Code |
|---|---|
| Allow scripts to run | Set-ExecutionPolicy RemoteSigned -Scope CurrentUser |
| Make errors catchable | -ErrorAction Stop / $ErrorActionPreference = 'Stop' |
| Handle a failure | try { … } catch { $_ } finally { … } |
| Install a module | Install-Module Name -Scope CurrentUser |
| Load a module | Import-Module Name |
| Schedule (Windows) | Register-ScheduledTask -TaskName … -Action … -Trigger … |
| Schedule (Linux/macOS) | crontab -e → 0 2 * * * pwsh -File ./job.ps1 |
Mini-Challenge: Backup Script
No blanks this time — just a brief and an outline. Write backup.ps1 yourself, test it with a real folder, then schedule it. This is the exact kind of script that runs in production every night.
# 🎯 MINI-CHALLENGE: backup.ps1
# Write a script that copies a project folder into a dated backup folder.
#
# 1. param(): $Source (string), $DestRoot (string, default 'C:\Backups')
# 2. Set $ErrorActionPreference = 'Stop'
# 3. Build a dated target path, e.g. C:\Backups\2026-06-15
# (hint: Join-Path $DestRoot (Get-Date -Format 'yyyy-MM-dd'))
# 4. try { Copy-Item $Source $target -Recurse -Force; Write-Output "Backed up to $target" }
# catch { Write-Error $_ }
# 5. BONUS: schedule it daily with Register-ScheduledTask (see section 5).
#
# ✅ Expected output (Source=C:\Project, on 2026-06-15):
# Backed up to C:\Backups\2026-06-15
# your code here🎉 Course Complete!
That's the whole PowerShell course. You can now:
- ✅ Save commands into reusable .ps1 scripts with param() inputs
- ✅ Unblock scripts safely with Set-ExecutionPolicy RemoteSigned
- ✅ Survive failures with try/catch/finally and -ErrorAction Stop
- ✅ Pull in extra power with Install-Module / Import-Module
- ✅ Run scripts on a timer with Register-ScheduledTask (or cron)
- ✅ Ship a real end-to-end automation, from dry-run to nightly schedule
Where next? Put it to work: automate a chore on your own machine (backups, log cleanup, a report). Then deepen your toolkit with the Git course to version-control your scripts, or the Python course for cross-platform scripting. Keep automating the boring stuff — that's the whole job.
Practice quiz
What file extension does a PowerShell script use?
- .ps1
- .psh
- .pow
- .sh
Answer: .ps1. PowerShell scripts are saved in files ending in .ps1.
Where must the param() block appear in a script?
- At the very end
- As the first code in the file
- Inside a function only
- Anywhere
Answer: As the first code in the file. param() must be the first executable code in a script file.
Which execution policy lets your local scripts run but gates downloaded ones?
- Restricted
- Bypass
- RemoteSigned
- Unrestricted
Answer: RemoteSigned. RemoteSigned runs your local scripts and requires downloaded ones to be signed.
Which block always runs, whether or not an error occurred?
- try
- catch
- trap
- finally
Answer: finally. The finally block always runs, making it ideal for cleanup.
How do you make a cmdlet error catchable by try/catch?
- Add -ErrorAction Stop
- Add -Catchable
- Use return
- Use Write-Error
Answer: Add -ErrorAction Stop. -ErrorAction Stop turns a non-terminating error into a catchable exception.
Which cmdlet downloads a module from the PowerShell Gallery?
- Import-Module
- Install-Module
- Get-Module
- Add-Module
Answer: Install-Module. Install-Module downloads and installs a module (once per machine).
Which cmdlet loads an installed module into the current session?
- Get-Module
- Install-Module
- Import-Module
- Use-Module
Answer: Import-Module. Import-Module loads a module's commands into the current session.
Which cmdlet registers a scheduled task on Windows?
- New-CronJob
- Add-Schedule
- Set-Timer
- Register-ScheduledTask
Answer: Register-ScheduledTask. Register-ScheduledTask creates a task from an action, trigger, and settings.
What is the cross-platform equivalent of a scheduled task on Linux/macOS?
- cron
- systemd-only
- Task Manager
- launchpad
Answer: cron. On Linux and macOS you use cron, e.g. a crontab entry, to schedule scripts.
Inside a catch block, how do you read the error message?
- $error.text
- $_.Exception.Message
- $catch.msg
- $LastError
Answer: $_.Exception.Message. $_ holds the thrown error; $_.Exception.Message is its text.
Continue this course
- Previous: File System and Registry
- Next: The Pipeline: Where-Object, Select-Object, Sort-Object — Filter, select, sort and group objects flowing through the pipeline
Frequently asked questions
What's the difference between a script and a function?
A script is a .ps1 file you run from the terminal (.\backup.ps1). A function is a named block of code defined inside a script or module that you call by name. Scripts are the unit you schedule; functions are how you organise the logic inside them.
Is changing the execution policy a security risk?
RemoteSigned is the recommended, safe default for a working machine: it lets scripts you wrote locally run, but still blocks scripts downloaded from the internet unless they're digitally signed. Avoid 'Unrestricted' or 'Bypass' on machines you care about — they disable that protection entirely.
Why didn't my try/catch catch the error?
Most cmdlet errors are 'non-terminating' — they print red text but don't throw, so catch never fires. Force them to throw with -ErrorAction Stop on the cmdlet, or set $ErrorActionPreference = 'Stop' once at the top of the script. Only terminating errors reach catch.
Do I need to Import-Module every time?
Install-Module is once per machine. In an interactive session modern PowerShell auto-imports a module the first time you use one of its commands, so Import-Module is often optional. But put it explicitly in scripts so they don't depend on auto-loading behaviour that might differ on another machine.
How do I schedule a script on macOS or Linux?
Register-ScheduledTask is Windows-only. On macOS/Linux, use cron: run `crontab -e` and add a line like `0 2 * * * pwsh -File /home/me/cleanup-logs.ps1` to run the script every day at 02:00. PowerShell itself is cross-platform; only the scheduler differs.