Iteractive calender PSU

Product: PowerShell Universal
Version: 2026.1.3

Trying to create a iteractive calender there you can input/delete/edit text on days but cant make it work, have someone done this or know a solution for it with PSU?

Are you looking to create a web app that functions in a siilar manner to an Outlook calendar, or create a .ics file to imort into a calendar or use MSGraph to update an existing Outlook calendar in real time?

I’ve made a script to export PSU schedules to a .ics for a visual representation of what will be running and when. I have plans in the future to switch it to MSGraph to update a calendar in real time. I’m happy to share what I have right now if it’s of use.

Hey @mikesimmons that actually sounds great.
One thing that I’ve been looking for is a more visual representation of scheduled jobs - sometimes if too many things are running at the same time, memory maxes out so it’s good to be able to visually see where there’s space in a calendar and shift things around.
If you’re willing to share I’d love to see what you’ve got to do that.

I’ll check to see if there’s any feature requests for something like this as it would be good out of the box.
The only other thing I have to compare it to is when I used to use teamcity there was a timeline view of schedules that helped in a similar way.

I would actually love to have this as well

I currently use a PSU dashboard to show scheduled jobs but having it in an Outlook calendar would be nice to share with the team.

As I was grabbing the code it did just occur to me that I schedule all my tasks using cron and that’s what the script specifically searches for and converts into calendar events.

It’s not going to work with simple scheule in current form, but a new function could be added to parse the other properties of Get-PSUSchedule if cron property is null.

<#
.DESCRIPTION
    Generates an iCalendar (.ics) file for scheduled Universal jobs.

.NOTES
    Created: 22/12/2025
    Reviewed: 22/12/2025

.CHANGELOG
	<< 01/01/1970 >> << Editor Name >>	<< Change Description >>

#>

[array]$CronObjects = (Get-PSUSchedule -Integrated | Where { -not $_.Paused} | Select Cron, Script)
[datetime]$StartDate = (Get-Date).AddMonths(-1)
[datetime]$EndDate = (Get-Date).AddMonths(1)
[string]$OutputFile = "$Repository\Calendar\Schedule.ics"

function Parse-Cron {
    param([string]$Expression, [string]$Summary)

    $parts = $Expression -split '\s+'
    if ($parts.Count -ne 5) {
        throw "Invalid cron format for '$Summary'. Expected: m h dom mon dow"
    }

    return @{
        Minute     = $parts[0]
        Hour       = $parts[1]
        DayOfMonth = $parts[2]
        Month      = $parts[3]
        DayOfWeek  = $parts[4]
        Summary    = $Summary
    }
}

function Match-Part {
    param($Value, $Part)

    if ($Part -eq "*") { return $true }

    foreach ($token in $Part -split ',') {
        if ($token -match '^\d+$') {
            if ([int]$token -eq $Value) { return $true }
        }
        elseif ($token -match '^(\d+)-(\d+)$') {
            if ($Value -ge [int]$matches[1] -and $Value -le [int]$matches[2]) { return $true }
        }
        elseif ($token -match '^\*/(\d+)$') {
            $step = [int]$matches[1]
            if ($Value % $step -eq 0) { return $true }
        }
    }
    return $false
}

function Matches-Cron {
    param(
        [datetime]$Date,
        [hashtable]$CronParts
    )

    return (Match-Part $Date.Minute $CronParts.Minute) -and
           (Match-Part $Date.Hour $CronParts.Hour) -and
           (Match-Part $Date.Day $CronParts.DayOfMonth) -and
           (Match-Part $Date.Month $CronParts.Month) -and
           (Match-Part ([int]$Date.DayOfWeek) $CronParts.DayOfWeek)
}

function Generate-ICS {
    param([array]$Events)

    $ics = @()
    $ics += "BEGIN:VCALENDAR"
    $ics += "VERSION:2.0"
    $ics += "PRODID:-//CronToICS//EN"
    $ics += "X-WR-CALNAME:Universal Cron Schedule"

    $StatusCheck = @()

    foreach ($event in $Events) {
        $uid = [guid]::NewGuid().ToString()
        $start = $event.Start.ToUniversalTime().ToString("yyyyMMdd'T'HHmm00'Z'")
        $end   = $event.End.ToUniversalTime().ToString("yyyyMMdd'T'HHmm00'Z'")

        if ($Event.Start -lt (Get-Date)) {
            $Category = 'Universal Historic'
        } else {
            $Category = 'Universal Scheduled'
        }

        $Status = $StatusCheck | Where {$_.Script -eq $Event.Script} | Select -ExpandProperty Status

        if (-not $Status) {
            $LastJob = Get-PSUJob -Script $Event.Script -Integrated | Select -First 1
            $Status = $LastJob.Status
            #$LastJob
            $StatusCheck += [PSCustomObject]@{
                Script = $Event.Script
                Status = $Status
            }
        }

        if ($Category -eq 'Universal Historic') {
            if ($Status) {
                $Category = "$Category, Universal $Status"
            }
            <#if ($Status -eq 'Completed') {
                $Category = "$Category,Universal Success"
            } elseif ($Status -eq 'Warning') {
                $Category = "$Category,Universal Warning"
            } elseif ($Status -eq 'Failed' -or $Status -eq 'Error') {
                $Category = "$Category,Universal Error"
            } elseif ($Status -eq 'Running') {
                $Category = "$Category,Universal Running"
            } elseif ($Status -eq 'Canceled') {
                $Category = "$Category,Universal Canceled"
            } #>
        }

        $ics += "BEGIN:VEVENT"
        $ics += "UID:$uid"
        $ics += "DTSTAMP:$start"
        $ics += "DTSTART:$start"
        $ics += "DTEND:$end"
        $ics += "SUMMARY:$($event.Summary)"
        $ics += "CATEGORIES:$Category"
        $ics += "END:VEVENT"
    }

    $ics += "END:VCALENDAR"
    return $ics -join "`r`n"
}


$events = @()

foreach ($obj in $CronObjects) {
    try {
        $cronParts = Parse-Cron $obj.cron $obj.script

        $current = $StartDate
        $Hours = $CronParts.Hour -split ','
        while ($current -le $EndDate) {
            Foreach ($Hour in $Hours) {
                $CronParts.Hour = $Hour

                if (Matches-Cron $current $cronParts) {
                    $events += [pscustomobject]@{
                        Start   = $current
                        End     = $current.AddMinutes(15) # default 15 min duration
                        Summary = $cronParts.Summary
                        Script  = $obj.Script
                    }
                }
            }
            $current = $current.AddMinutes(1)
        }
    }

    catch {
        Write-Error $_.Exception.Message
    }
}

if (-not $events) {
    Write-Warning "No events generated."
} else {
    $icsContent = Generate-ICS -Events $events
    Set-Content -Path $OutputFile -Value $icsContent -Encoding UTF8 -Force

    Write-Host "ICS file created with $($events.Count) events."
}

    

It also adds categories, so you can set colours for these in Outlook so you can see at a glance if anything went wrong on the latest run - it could use a little work but it’s not high on my priority list.