Skip to main content
    Courses/PowerShell/Automation and Task Scheduling

    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.

    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.

    Worked example: save-me.ps1
    # ===== 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')"
    Output
    # 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-15
    Save as save-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.

    Worked example: allow your scripts to run
    # 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 gated
    Output
    Restricted
    RemoteSigned
    Run in a PowerShell terminal. -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: catch a missing file
    # 🎯 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.
    Output
    WARNING: Could not read the file: Cannot find path 'C:\Reports\sales.csv' because it does not exist.
    Done.
    Fill in the ___ 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.

    Worked example: install, import, and use a module
    # 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
    Output
    RuleName                        Severity   Line  Message
    --------                        --------   ----  -------
    PSAvoidUsingWriteHost           Warning       8  Avoid Write-Host; use Write-Output.
    PSUseDeclaredVarsMoreThanAssign Warning      12  The variable 'temp' is assigned but never used.
    Run in a PowerShell terminal. 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.

    Worked example: a nightly scheduled task
    # 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
    Output
    TaskPath   TaskName        State
    --------   --------        -----
    \          NightlyBackup   Ready
    Run in an Administrator PowerShell terminal on Windows.

    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.

    Worked example: cleanup-logs.ps1
    # ============================================================
    #  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 $_
    }
    Output
    # 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.
    Save as 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 $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

    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

    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: build backup.ps1
    # 🎯 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
    Write the script from the outline, save as backup.ps1, and check your output against the expected line.

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

    Sign up for free to track which lessons you've completed and get learning reminders.

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service