Chapter 12Lesson 05~170 minutes

Scheduled Tasks, Local Administration, and Platform Boundaries

Design scheduler and local-administration automation around capability detection, Windows Task Scheduler, native OS tools, thin platform adapters, least privilege, and appropriate control planes.

Learning objectives

  • Compare Windows Task Scheduler, cron/systemd timers, launchd, and CI schedulers conceptually.
  • Inspect Windows scheduled tasks and build an unregistered disposable definition safely.
  • Recognize registration as a real mutation and avoid inventing unsupported WhatIf behavior.
  • Detect exact modules/native commands before using platform features.
  • Isolate scheduler/local-administration adapters from shared business logic.
  • Choose between PowerShell cmdlets, native OS tools, configuration management, and DevOps platforms.

1. Scheduling is an operating-system/platform concern

A scheduler decides when work should run independently of an interactive shell. Windows Task Scheduler, cron, systemd timers, macOS launchd, and CI/CD schedulers all solve “run this later or repeatedly,” but they differ in identity, environment, retry behavior, logs, calendars, and deployment model.

Do not hide those differences behind a fake universal command. Instead, keep shared business logic in a normal script/function and isolate scheduler-specific registration in thin platform adapters.

2. Windows ScheduledTasks cmdlets are a Windows-specific path

The Windows ScheduledTasks module can inspect, create, register, start, stop, and unregister Task Scheduler definitions. These cmdlets are documented in the Windows/Windows Server PowerShell surface, not as a cross-platform PowerShell 7 abstraction.

$scheduledTasksAvailable = $IsWindows -and [bool](Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue)
[pscustomobject]@{
    IsWindows               = $IsWindows
    ScheduledTasksAvailable = $scheduledTasksAvailable
    ModuleFound             = [bool](Get-Module -ListAvailable ScheduledTasks)
}

3. Start with read-only task discovery

if ($IsWindows -and (Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue)) {
    Get-ScheduledTask |
        Select-Object -First 15 TaskPath,TaskName,State |
        Format-Table -AutoSize
} else {
    'Windows ScheduledTasks cmdlets are not available here.'
}

4. Build a harmless task definition without registering it

New-ScheduledTaskAction, New-ScheduledTaskTrigger, and New-ScheduledTask can build definition objects. New-ScheduledTask does not register the task automatically, so it is a useful safe teaching boundary.

if ($IsWindows -and (Get-Command New-ScheduledTask -ErrorAction SilentlyContinue)) {
    $pwsh = (Get-Command pwsh -CommandType Application).Source
    $action = New-ScheduledTaskAction -Execute $pwsh -Argument '-NoProfile -Command "exit 0"'
    $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddHours(1)
    $definition = New-ScheduledTask -Action $action -Trigger $trigger -Description 'Chapter 12 disposable definition only'
    $definition | Select-Object Description,Actions,Triggers
}

5. Registration is a real system mutation—and Register-ScheduledTask has no -WhatIf

Register-ScheduledTask changes Task Scheduler state. Its current syntax does not provide -WhatIf, so do not invent a preview contract that does not exist. If a real lab ever registers a task, use a unique disposable name, a harmless command, an explicit cleanup step, and the least privilege required.

# Windows-only mutation example, intentionally NOT executed in this course lab:
# $taskName = 'DevOpsAcademy-Ch12-' + [guid]::NewGuid().ToString('N')
# Register-ScheduledTask -TaskName $taskName -InputObject $definition
# ...verify the disposable task...
# Unregister-ScheduledTask -TaskName $taskName -Confirm:$false

6. Linux scheduling: cron and systemd timers are native tools

Linux hosts may use cron, systemd timers, Kubernetes CronJobs, or a centralized automation platform. Before writing a cron entry, ask who owns deployment, logs, environment variables, secrets, retry policy, and observability. A local cron job may be the wrong abstraction inside a managed fleet.

if ($IsLinux) {
    [pscustomobject]@{
        Crontab       = [bool](Get-Command crontab -ErrorAction SilentlyContinue)
        Systemctl     = [bool](Get-Command systemctl -ErrorAction SilentlyContinue)
        TimersSupport = if (Get-Command systemctl -ErrorAction SilentlyContinue) { 'Check systemctl list-timers' } else { 'Not detected' }
    }
}

7. CI/CD schedulers are often a better operational home

If the task belongs to a repository, release process, or shared environment, a CI/CD scheduler can provide versioned configuration, centralized credentials, logs, approvals, concurrency control, and auditability. Local OS schedulers are appropriate when the work truly belongs to one host's lifecycle.

WorkBetter default
Host-local maintenance tied to one machineOS scheduler / configuration manager
Repository validation or periodic buildCI/CD scheduler
Fleet-wide state convergenceConfiguration-management/orchestration platform
Kubernetes workloadKubernetes CronJob or platform controller
One-off operator actionExplicit PowerShell command/runbook, not a hidden recurring task

8. Local users/groups and firewall are separate privileged platform APIs

Local account and firewall administration is security-sensitive and highly platform-specific. Current PowerShell 7 module availability differs from Windows PowerShell-era documentation; do not assume Get-LocalUser or Windows NetSecurity cmdlets exist merely because the host is running PowerShell.

This chapter keeps those areas optional and read-only. Chapter 16 addresses credentials, secrets, signing, and least privilege in depth.

$capabilities = [ordered]@{
    GetLocalUser       = [bool](Get-Command Get-LocalUser -ErrorAction SilentlyContinue)
    GetNetFirewallRule = [bool](Get-Command Get-NetFirewallRule -ErrorAction SilentlyContinue)
    GetScheduledTask   = [bool](Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue)
}
[pscustomobject]$capabilities

9. Capability detection beats OS-name assumptions

$IsWindows, $IsLinux, and $IsMacOS are useful routing signals, but a platform name does not prove a module or native tool is installed. Detect the exact command you intend to use.

function Test-CommandCapability {
    param([Parameter(Mandatory)][string]$Name)
    $cmd = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1
    [pscustomobject]@{
        Name      = $Name
        Available = $null -ne $cmd
        Type      = if ($cmd) { $cmd.CommandType.ToString() } else { $null }
        Source    = if ($cmd) { $cmd.Source } else { $null }
    }
}

'Get-ScheduledTask','systemctl','crontab','launchctl' | ForEach-Object { Test-CommandCapability $_ }

10. Keep shared logic cross-platform; isolate platform adapters

A good host-automation architecture has a shared layer that accepts/returns stable objects and small adapters that know platform-specific commands. The shared layer should not parse systemctl output, know Task Scheduler XML, or contain launchd-specific syntax.

function Get-SchedulerCapability {
    [CmdletBinding()]
    param()

    if ($IsWindows -and (Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue)) {
        return [pscustomobject]@{ Platform='Windows'; Scheduler='Task Scheduler'; Adapter='ScheduledTasks'; Available=$true }
    }
    if ($IsLinux -and (Get-Command systemctl -ErrorAction SilentlyContinue)) {
        return [pscustomobject]@{ Platform='Linux'; Scheduler='systemd timers'; Adapter='systemctl'; Available=$true }
    }
    if ($IsLinux -and (Get-Command crontab -ErrorAction SilentlyContinue)) {
        return [pscustomobject]@{ Platform='Linux'; Scheduler='cron'; Adapter='crontab'; Available=$true }
    }
    if ($IsMacOS -and (Get-Command launchctl -ErrorAction SilentlyContinue)) {
        return [pscustomobject]@{ Platform='macOS'; Scheduler='launchd'; Adapter='launchctl'; Available=$true }
    }
    [pscustomobject]@{ Platform=$PSVersionTable.OS; Scheduler=$null; Adapter=$null; Available=$false }
}

Get-SchedulerCapability

11. The scheduled unit should still be a normal testable script

Put real business behavior in a normal script with parameters, validation, logs, and deterministic exit behavior. Then Task Scheduler, cron, systemd, launchd, or CI calls that entry point. This keeps the automation testable outside the scheduler.

# Example shared entry point shape
param(
    [ValidateSet('dev','staging','prod')]
    [string]$Environment = 'dev',
    [string]$OutputPath = (Join-Path $PSScriptRoot 'status.json')
)

$result = [pscustomobject]@{
    Environment = $Environment
    CapturedUtc = [datetime]::UtcNow
    Computer    = [Environment]::MachineName
    Status      = 'ok'
}
$result | ConvertTo-Json | Set-Content -LiteralPath $OutputPath -Encoding utf8
$result

12. Decision guide: cmdlet, native tool, configuration manager, or DevOps platform?

QuestionChoose
PowerShell has a current supported cmdlet for this exact local capabilityUse the cmdlet, with ShouldProcess/privilege controls where applicable
The OS owns the feature and PowerShell has no supported abstractionUse the native OS tool behind a small adapter
State must converge across many hosts repeatedlyUse configuration management/orchestration rather than ad-hoc loops
Work belongs to build/release/repository lifecycleUse a CI/CD platform scheduler
The logic is shared but registration differs by OSKeep logic common; isolate scheduler/platform adapters

13. Lab: inventory scheduler capabilities without changing the host

$report = [pscustomobject]@{
    CapturedUtc = [datetime]::UtcNow
    Platform    = if ($IsWindows) {'Windows'} elseif ($IsLinux) {'Linux'} elseif ($IsMacOS) {'macOS'} else {'Other'}
    Scheduler   = Get-SchedulerCapability
    Commands    = @(
        Test-CommandCapability 'Get-ScheduledTask'
        Test-CommandCapability 'systemctl'
        Test-CommandCapability 'crontab'
        Test-CommandCapability 'launchctl'
    )
}
$report | ConvertTo-Json -Depth 6

# Windows-only, definition-only optional extension:
if ($IsWindows -and (Get-Command New-ScheduledTask -ErrorAction SilentlyContinue)) {
    $pwsh = (Get-Command pwsh -CommandType Application).Source
    $action = New-ScheduledTaskAction -Execute $pwsh -Argument '-NoProfile -Command "exit 0"'
    $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddHours(1)
    New-ScheduledTask -Action $action -Trigger $trigger -Description 'Unregistered Chapter 12 definition' |
        Select-Object Description,Actions,Triggers
}

14. Warning signs that local administration is becoming the wrong layer

  • Dozens or hundreds of hosts each need the same scheduled configuration.
  • The script needs a home-grown credential database or secret distribution system.
  • You need approvals, retries, fleet concurrency, centralized audit logs, or drift detection.
  • Platform-specific branches dominate the business logic.
  • Operators cannot tell what version of a scheduled script is deployed where.

Those are signals to move toward configuration management, endpoint management, orchestration, CI/CD, or another purpose-built control plane.

15. Chapter 12 operating model

The chapter's reusable pattern is:

  1. Detect: platform and exact command/module capabilities.
  2. Observe: collect structured state before change.
  3. Plan: identify target, privileges, dependencies, and rollback.
  4. Preview: use -WhatIf when the cmdlet actually supports it; never fabricate preview semantics.
  5. Act: make the smallest authorized change.
  6. Verify: poll observable state with a deadline.
  7. Record: emit machine-readable evidence and exit status.

16. Common mistakes

  • Assuming a Windows module is part of cross-platform PowerShell.
  • Using OS-name checks without verifying the exact command exists.
  • Registering scheduled tasks in a training lab without a unique name and cleanup plan.
  • Claiming Register-ScheduledTask supports -WhatIf when it does not.
  • Embedding business logic directly inside scheduler-specific command strings.
  • Building a bespoke fleet scheduler when a configuration manager or CI/CD platform already solves the control-plane problem.

17. Knowledge check

Question 1. Why should scheduler registration be separated from the script being scheduled?

Question 2. Does Register-ScheduledTask currently provide -WhatIf?

Question 3. Why use Get-Command in addition to $IsWindows/$IsLinux?

Question 4. When is a CI/CD scheduler preferable to a local OS scheduler?

Question 5. What is the core Chapter 12 platform pattern?

18. Chapter summary and next bridge

You can now apply PowerShell fundamentals to host operations without erasing platform boundaries. Process automation is broadly cross-platform; PowerShell service cmdlets, CimCmdlets, Windows Event Log, Get-ComputerInfo, and ScheduledTasks are Windows-specific in the current environment. Reliable operations code detects exact capabilities, isolates native adapters, previews only where supported, uses least privilege, polls with deadlines, and preserves evidence. Chapter 13 moves these habits to networking and HTTP/REST integrations, where remote failures and explicit contracts become the next operational boundary.

19. Authoritative references

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.