Skip to content
ConsoleMonster

Scheduled task health

Read every task's last result and next run, then follow each failure from trigger to the action that broke.

Demos Reports Showcase

Use it when a job did not run overnight and you need to know whether the trigger, the task or the action failed.

  • Static
  • 100×34
  • PowerShell 7.4+
  • .NET 8.0+
Scheduled task health 100×3424-bitFull

How it's built

The whole program in one file, with its sample data at the top. It drew the recording above; all three styles produce the same frame bytes.

PowerShellscheduled-tasks.ps1View on GitHub
# Check scheduled task health and trace each failure from its trigger to the action that broke.
Import-Module -Name ConsoleMonster

# Data: fictional tasks on one server; Task Scheduler is not queried and nothing is run.
$tasks = @(
    [pscustomobject] @{ Task = 'Nightly SQL backup'; Trigger = 'Daily 02:00'
        LastRun = '10-26 02:00'; Result = '0x1'; NextRun = '10-27 02:00'; Status = 'Fail' }
    [pscustomobject] @{ Task = 'Certificate renewal'; Trigger = 'Mon 03:00'; LastRun = '10-26 03:00'
        Result = '0x80070005'; NextRun = '11-02 03:00'; Status = 'Fail' }
    [pscustomobject] @{ Task = 'Report export'; Trigger = 'Daily 06:30'; LastRun = '10-26 06:30'
        Result = '0x41301'; NextRun = '10-27 06:30'; Status = 'Info' }
    [pscustomobject] @{ Task = 'Log cleanup'; Trigger = 'Daily 01:00'; LastRun = '10-26 01:00'
        Result = '0x0'; NextRun = '10-27 01:00'; Status = 'Pass' }
    [pscustomobject] @{ Task = 'Inventory sync'; Trigger = 'Hourly'; LastRun = '10-26 09:00'
        Result = '0x0'; NextRun = '10-26 10:00'; Status = 'Pass' }
    [pscustomobject] @{ Task = 'Defragment volumes'; Trigger = 'Sun 04:00'; LastRun = '10-25 04:00'
        Result = '0x0'; NextRun = '11-01 04:00'; Status = 'Pass' }
    [pscustomobject] @{ Task = 'Legacy FTP upload'; Trigger = 'Disabled'; LastRun = '09-30 23:00'
        Result = '0x0'; NextRun = '-'; Status = 'Skip' }
)
$failures = @(
    [pscustomobject] @{ Task = 'Nightly SQL backup'; Trigger = 'Daily 02:00'
        Action = 'Backup-Sql.ps1'; Outcome = 'exit 1'; Cause = 'Target share \\nas-01\sql is full'
        Fix = 'Free space or move the target, then run the task once by hand.' }
    [pscustomobject] @{ Task = 'Certificate renewal'; Trigger = 'Mon 03:00'
        Action = 'Renew-Certificates.ps1'; Outcome = 'access denied'
        Cause = 'svc-certs lost Log on as a batch job'
        Fix = 'Restore the right in the server baseline policy.' }
)

# Layout: the task table first, then one trigger → task → action trace per failure, side by side.
$report = New-ConsoleRows -Gap 1 -Content {
    New-ConsoleRule -Title '[bold]Scheduled tasks[/] [muted]· app-01.adatum.example.test · 2026-10-26 09:05[/]'
    New-ConsoleTable -InputObject $tasks -Property Task, Trigger, LastRun, Result, NextRun,
        Status -Expand
    New-ConsoleColumns -Content {
        foreach ($failure in $failures) {
            New-ConsolePanel -Title "$($failure.Task) · $($failure.Outcome)" -Status Fail -Content {
                New-ConsoleRows -Gap 1 -Content {
                    @(
                        [pscustomobject] @{ Kind = 'Node'; Key = 'trigger'; Label = $failure.Trigger
                            Status = 'Pass' }
                        [pscustomobject] @{ Kind = 'Node'; Key = 'task'; Label = $failure.Task
                            Status = 'Pass' }
                        [pscustomobject] @{ Kind = 'Node'; Key = 'action'; Label = $failure.Action
                            Status = 'Fail' }
                        [pscustomobject] @{ Kind = 'Edge'; Key = 'fires'; From = 'trigger'
                            To = 'task'; Label = 'fired' }
                        [pscustomobject] @{ Kind = 'Edge'; Key = 'runs'; From = 'task'
                            To = 'action'; Label = $failure.Outcome; Status = 'Fail'; Cut = $true }
                    ) | New-ConsoleFlow
                    New-ConsoleKeyValue -KeyRole Fail -Content {
                        [pscustomobject] @{ Key = 'Cause'; Value = $failure.Cause }
                        [pscustomobject] @{ Key = 'Fix'; Value = $failure.Fix }
                    }
                }
            }
        }
    }
}

# Behaviour: print once; fix the cause, then start the task by hand and rerun this check.
$report | Show-Console
C#Program.csView on GitHub
// Check scheduled task health and trace each failure from its trigger to the action that broke.
using ConsoleMonster;
using ConsoleMonster.Layout;
using ConsoleMonster.Presenters;
using ConsoleMonster.Styling;
using ConsoleMonster.Widgets;

// Data: fictional tasks on one server; Task Scheduler is not queried and nothing is run.
var tasks = new TaskRow[]
{
    new("Nightly SQL backup", "Daily 02:00", "10-26 02:00", "0x1", "10-27 02:00", "Fail"),
    new("Certificate renewal", "Mon 03:00", "10-26 03:00", "0x80070005", "11-02 03:00", "Fail"),
    new("Report export", "Daily 06:30", "10-26 06:30", "0x41301", "10-27 06:30", "Info"),
    new("Log cleanup", "Daily 01:00", "10-26 01:00", "0x0", "10-27 01:00", "Pass"),
    new("Inventory sync", "Hourly", "10-26 09:00", "0x0", "10-26 10:00", "Pass"),
    new("Defragment volumes", "Sun 04:00", "10-25 04:00", "0x0", "11-01 04:00", "Pass"),
    new("Legacy FTP upload", "Disabled", "09-30 23:00", "0x0", "-", "Skip")
};
var failures = new Failure[]
{
    new("Nightly SQL backup", "Daily 02:00", "Backup-Sql.ps1", "exit 1",
        @"Target share \\nas-01\sql is full",
        "Free space or move the target, then run the task once by hand."),
    new("Certificate renewal", "Mon 03:00", "Renew-Certificates.ps1", "access denied",
        "svc-certs lost Log on as a batch job", "Restore the right in the server baseline policy.")
};

// Layout: the task table first, then one trigger → task → action trace per failure, side by side.
var table = Table.For(tasks)
    .Column(row => row.Task)
    .Column(row => row.Trigger)
    .Column(row => row.LastRun)
    .Column(row => row.Result)
    .Column(row => row.NextRun)
    .Column(row => row.Status, column => column.Presenter = CellPresenters.StatusWord);
table.Expand = true;

var traces = new Columns();
foreach (var failure in failures)
{
    var flow = new Flow();
    flow.Add(new FlowNode("trigger", failure.Trigger, StatusKind.Pass));
    flow.Add(new FlowNode("task", failure.Task, StatusKind.Pass));
    flow.Add(new FlowNode("action", failure.Action, StatusKind.Fail));
    flow.Add(new FlowEdge("fires", "trigger", "task", "fired"));
    flow.Add(new FlowEdge("runs", "task", "action", failure.Outcome, StatusKind.Fail, true));
    var details = new KeyValueList { KeyRole = ThemeToken.Fail }
        .Add("Cause", failure.Cause)
        .Add("Fix", failure.Fix);
    traces.Add(new Panel(
        new Rows(flow, details) { Gap = 1 },
        $"{failure.Task} · {failure.Outcome}") { Status = StatusKind.Fail });
}

var report = new Rows(
    new Rule("[bold]Scheduled tasks[/] [muted]· app-01.adatum.example.test · 2026-10-26 09:05[/]"),
    table,
    traces)
{
    Gap = 1
};

// Behaviour: print once; fix the cause, then start the task by hand and rerun this check.
Terminal.Write(report);

record TaskRow(string Task, string Trigger, string LastRun, string Result, string NextRun,
    string Status);

record Failure(string Task, string Trigger, string Action, string Outcome, string Cause,
    string Fix);
C# fluentProgram.Fluent.csView on GitHub
// Check scheduled task health and trace each failure from its trigger to the action that broke.
using ConsoleMonster;
using ConsoleMonster.Layout;
using ConsoleMonster.Presenters;
using ConsoleMonster.Styling;
using ConsoleMonster.Widgets;

// Data: fictional tasks on one server; Task Scheduler is not queried and nothing is run.
var tasks = new TaskRow[]
{
    new("Nightly SQL backup", "Daily 02:00", "10-26 02:00", "0x1", "10-27 02:00", "Fail"),
    new("Certificate renewal", "Mon 03:00", "10-26 03:00", "0x80070005", "11-02 03:00", "Fail"),
    new("Report export", "Daily 06:30", "10-26 06:30", "0x41301", "10-27 06:30", "Info"),
    new("Log cleanup", "Daily 01:00", "10-26 01:00", "0x0", "10-27 01:00", "Pass"),
    new("Inventory sync", "Hourly", "10-26 09:00", "0x0", "10-26 10:00", "Pass"),
    new("Defragment volumes", "Sun 04:00", "10-25 04:00", "0x0", "11-01 04:00", "Pass"),
    new("Legacy FTP upload", "Disabled", "09-30 23:00", "0x0", "-", "Skip")
};
var failures = new Failure[]
{
    new("Nightly SQL backup", "Daily 02:00", "Backup-Sql.ps1", "exit 1",
        @"Target share \\nas-01\sql is full",
        "Free space or move the target, then run the task once by hand."),
    new("Certificate renewal", "Mon 03:00", "Renew-Certificates.ps1", "access denied",
        "svc-certs lost Log on as a batch job", "Restore the right in the server baseline policy.")
};

// Layout: the task table first, then one trigger → task → action trace per failure, side by side.
var table = Ui.Table(tasks)
    .Column(row => row.Task)
    .Column(row => row.Trigger)
    .Column(row => row.LastRun)
    .Column(row => row.Result)
    .Column(row => row.NextRun)
    .Column(row => row.Status, column => column.Presenter(CellPresenters.StatusWord))
    .Expand();
var traces = Ui.Columns();
foreach (var failure in failures)
{
    var flow = Ui.Flow();
    flow.Add(new FlowNode("trigger", failure.Trigger, StatusKind.Pass));
    flow.Add(new FlowNode("task", failure.Task, StatusKind.Pass));
    flow.Add(new FlowNode("action", failure.Action, StatusKind.Fail));
    flow.Add(new FlowEdge("fires", "trigger", "task", "fired"));
    flow.Add(new FlowEdge("runs", "task", "action", failure.Outcome, StatusKind.Fail, true));
    var details = Ui.KeyValue()
        .KeyRole(ThemeToken.Fail)
        .Add("Cause", failure.Cause)
        .Add("Fix", failure.Fix);
    traces.Add(Ui.Panel().Content(content =>
    {
        content.Rows(r => r.Gap(1).Content(c =>
        {
            c.Add(flow);
            c.Add(details);
        }));
    }).Title($"{failure.Task} · {failure.Outcome}").Status(StatusKind.Fail));
}

var report = Ui.Rows().Gap(1).Content(c =>
{
    c.Rule("[bold]Scheduled tasks[/] [muted]· app-01.adatum.example.test · 2026-10-26 09:05[/]");
    c.Add(table);
    c.Add(traces);
});

// Behaviour: print once; fix the cause, then start the task by hand and rerun this check.
report.Show();

record TaskRow(string Task, string Trigger, string LastRun, string Result, string NextRun,
    string Status);

record Failure(string Task, string Trigger, string Action, string Outcome, string Cause,
    string Fix);

Run it yourself

Three steps in PowerShell, four in C#. Every command copies with one click.

  1. Install the module

    Once. Windows PowerShell 5.1 or PowerShell 7 on Windows, macOS or Linux.

    Install-Module ConsoleMonster -Scope CurrentUser
  2. Save the program

    Download it, or copy it from above into scheduled-tasks.ps1.

  3. Run it

    From the folder you saved it in.

    ./scheduled-tasks.ps1
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n ScheduledTasks; cd ScheduledTasks
  2. Save the project

    The project selects one C# style at a time and restores ConsoleMonster.

  3. Save both programs

    Replace Program.cs and save Program.Fluent.cs beside it.

  4. Run it

    dotnet run
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n ScheduledTasks; cd ScheduledTasks
  2. Save the project

    The project selects one C# style at a time and restores ConsoleMonster.

  3. Save both programs

    Replace Program.cs and save Program.Fluent.cs beside it.

  4. Run it

    dotnet run --property:DemoStyle=Fluent

Or skip the copying. Every demo ships with the module and the project template.

Show-ConsoleDemo scheduled-tasks
Export-ConsoleDemo scheduled-tasks -Path ./scheduled-tasks.ps1
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo scheduled-tasks -n ScheduledTasks
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo scheduled-tasks --DemoStyle Fluent -n ScheduledTasks

Make it yours

Start with these, then read the rest of the file.

  1. Fill the task rows from Get-ScheduledTask piped to Get-ScheduledTaskInfo for LastRunTime, LastTaskResult and NextRunTime.

  2. Translate the result codes you see most often into a cause and a fix for the failure panels.

  3. Point it at several servers and add a Server column before the Task column.

More reports

All demos