Lesson 7 • Final Lesson
Automation and Task Scheduling ⚙️
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.
.ps1 files and run them in a PowerShell terminal (pwsh) on your own machine — the scheduled-task and policy commands need Windows (and Administrator for some). Each block shows the output you should expect.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
try/catch is the smoke alarm — when something goes wrong, you get a clear alert instead of a silent fire.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')"# First run with no argument (uses the default):
PS C:\Scripts> .\save-me.ps1
Hello, world!
Today is 2026-06-15
# Run again, passing a value for -Name:
PS C:\Scripts> .\save-me.ps1 -Name 'Sam'
Hello, Sam!
Today is 2026-06-15save-me.ps1 and run it in a PowerShell terminal (pwsh) on your own machine.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.
# Windows BLOCKS scripts by default to stop you running malware by accident.
# Out of the box you'll hit this when you try to run save-me.ps1:
# .\save-me.ps1 : File ... cannot be loaded because running scripts
# is disabled on this system.
# 1) See the current policy
Get-ExecutionPolicy
# Restricted <- the default: no scripts allowed at all
# 2) Allow YOUR own scripts but require downloaded ones to be signed.
# RemoteSigned is the recommended setting for a dev machine.
# -Scope CurrentUser does NOT need Administrator rights.
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
# 3) Confirm it stuck
Get-ExecutionPolicy
# RemoteSigned <- your scripts now run; the internet's still gatedRestricted
RemoteSigned-Scope CurrentUser means you don't need Administrator.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.WARNING: Could not read the file: Cannot find path 'C:\Reports\sales.csv' because it does not exist.
Done.___ blanks, save as a .ps1, and run it. Your output should match the expected lines.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.ps1RuleName Severity Line Message
-------- -------- ---- -------
PSAvoidUsingWriteHost Warning 8 Avoid Write-Host; use Write-Output.
PSUseDeclaredVarsMoreThanAssign Warning 12 The variable 'temp' is assigned but never used.Install-Module ... -Scope CurrentUser avoids needing Administrator.5. 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 # deleteTaskPath TaskName State
-------- -------- -----
\ NightlyBackup ReadyPutting 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 $_
}# Dry-run first to see what WOULD be deleted (nothing is removed):
PS C:\Scripts> .\cleanup-logs.ps1 -OlderThan 30 -WhatIf
What if: Performing the operation "Remove File" on target "C:\Logs\app-2026-04-02.log".
What if: Performing the operation "Remove File" on target "C:\Logs\app-2026-04-09.log".
2026-06-15T09:12:04 Removed 2 file(s) older than 30 days.
# Happy with it? Run for real:
PS C:\Scripts> .\cleanup-logs.ps1 -OlderThan 30
2026-06-15T09:12:31 Removed 2 file(s) older than 30 days.cleanup-logs.ps1. Always test with -WhatIf first, then schedule it with the task from section 5.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 $msgis how you find out what happened at 2am. - 💡 Test with
-WhatIfbefore 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 yourtry/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
catchnever runs and the script keeps going — the error was non-terminating. Add-ErrorAction Stopto 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 withJoin-Pathinstead of typingC:\Logsdirectly. - 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 CurrentUserthenImport-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 |
Frequently Asked Questions
Q: 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.
Q: 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.
Q: 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.
Q: 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.
Q: 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.
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 herebackup.ps1, and check your output against the expected line.🎉 Course Complete!
That's the whole PowerShell course. You can now:
- ✅ Save commands into reusable
.ps1scripts withparam()inputs - ✅ Unblock scripts safely with
Set-ExecutionPolicy RemoteSigned - ✅ Survive failures with
try/catch/finallyand-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.
Sign up for free to track which lessons you've completed and get learning reminders.