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

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.ps1

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  # delete

Putting 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

Common Errors (and the fix)

📋 Quick Reference

TaskCode
Allow scripts to runSet-ExecutionPolicy RemoteSigned -Scope CurrentUser
Make errors catchable-ErrorAction Stop / $ErrorActionPreference = 'Stop'
Handle a failuretry { … } catch { $_ } finally { … }
Install a moduleInstall-Module Name -Scope CurrentUser
Load a moduleImport-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:

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

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.

Links on this page