All scripts
Microsoft 365 143

Get-PlannerTaskOverdueReport

Reports all overdue tasks across Microsoft Planner plans in a Microsoft 365 group or the whole tenant, using Microsoft Graph, so you can see who is behind without opening every plan by hand.

Get-PlannerTaskOverdueReport.ps1
<#
.SYNOPSIS
    Reports overdue tasks across Microsoft Planner plans using Microsoft Graph.

.DESCRIPTION
    Connects to Microsoft Graph and walks every Microsoft 365 group the signed in
    account can see, pulls each group's Planner plans, and lists every task that
    is still open with a due date in the past. Useful for a quick sweep before a
    status meeting instead of opening each plan by hand.

.AUTHOR
    Shehryar Hassan

.EXAMPLE
    .\Get-PlannerTaskOverdueReport.ps1
    Lists every overdue Planner task the signed in account has access to, grouped by plan.

.EXAMPLE
    .\Get-PlannerTaskOverdueReport.ps1 -GroupId "11111111-2222-3333-4444-555555555555" -ExportPath "C:\Reports\overdue-tasks.csv"
    Scopes the report to one Microsoft 365 group and exports the results to a CSV file.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [string]$GroupId,

    [Parameter(Mandatory = $false)]
    [string]$ExportPath
)

if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Groups)) {
    Write-Warning "Microsoft.Graph module is not installed. Run: Install-Module Microsoft.Graph -Scope CurrentUser"
    return
}

Import-Module Microsoft.Graph.Groups
Import-Module Microsoft.Graph.Planner

Connect-MgGraph -Scopes "Group.Read.All", "Tasks.Read" -NoWelcome

$today = Get-Date

if ($GroupId) {
    $groups = @(Get-MgGroup -GroupId $GroupId)
} else {
    $groups = Get-MgGroup -All -Filter "resourceProvisioningOptions/Any(x:x eq 'Team')" -ErrorAction SilentlyContinue
    if (-not $groups) {
        $groups = Get-MgGroup -All
    }
}

$results = foreach ($group in $groups) {
    $plans = Get-MgGroupPlannerPlan -GroupId $group.Id -ErrorAction SilentlyContinue
    foreach ($plan in $plans) {
        $tasks = Get-MgPlannerPlanTask -PlannerPlanId $plan.Id -ErrorAction SilentlyContinue
        foreach ($task in $tasks) {
            if ($task.PercentComplete -lt 100 -and $task.DueDateTime -and ([datetime]$task.DueDateTime) -lt $today) {
                $assignees = @()
                if ($task.Assignments) {
                    $assignees = $task.Assignments.AdditionalProperties.Keys
                }
                [PSCustomObject]@{
                    Group       = $group.DisplayName
                    Plan        = $plan.Title
                    Task        = $task.Title
                    DueDate     = ([datetime]$task.DueDateTime).ToString("yyyy-MM-dd")
                    DaysOverdue = [math]::Round(($today - [datetime]$task.DueDateTime).TotalDays)
                    PercentDone = $task.PercentComplete
                    AssignedTo  = ($assignees -join ", ")
                }
            }
        }
    }
}

if (-not $results) {
    Write-Output "No overdue Planner tasks found."
    return
}

$results = $results | Sort-Object DaysOverdue -Descending

if ($ExportPath) {
    $results | Export-Csv -Path $ExportPath -NoTypeInformation
    Write-Output "Exported $($results.Count) overdue tasks to $ExportPath"
} else {
    $results | Format-Table -AutoSize
}

Read it before you run it, and test in a safe tenant first.