All scripts
Microsoft 365 95

Microsoft To Do Task List Report

Reports on Microsoft To Do task lists and task counts for each user in your tenant, using Microsoft Graph. Handy before offboarding someone or when you just want a picture of how To Do is actually being used.

Get-MicrosoftToDoTaskListReport.ps1
<#
.SYNOPSIS
    Reports on Microsoft To Do task lists and task counts across your tenant's users.

.DESCRIPTION
    Loops through a list of users (or every licensed user in the tenant) and uses
    Microsoft Graph to pull each person's To Do task lists, along with open and
    completed task counts per list. Useful before offboarding someone, or when
    you just want a picture of how To Do is actually being used instead of
    guessing. Requires the Microsoft.Graph.Users and Microsoft.Graph.Tasks
    modules, and delegated or application permissions for Tasks.Read.All and
    User.Read.All.

.AUTHOR
    Shehryar Hassan

.EXAMPLE
    Connect-MgGraph -Scopes "Tasks.Read.All","User.Read.All"
    .\Get-MicrosoftToDoTaskListReport.ps1 -OutputPath "C:\Reports\ToDoReport.csv"

.EXAMPLE
    .\Get-MicrosoftToDoTaskListReport.ps1 -UserPrincipalName "jane.doe@contoso.com" -OutputPath ".\jane-todo.csv"
#>

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

    [Parameter(Mandatory = $false)]
    [string]$OutputPath = ".\ToDoTaskListReport.csv"
)

if (-not (Get-MgContext)) {
    Write-Error "Not connected to Microsoft Graph. Run Connect-MgGraph -Scopes 'Tasks.Read.All','User.Read.All' first."
    return
}

if ($UserPrincipalName) {
    $users = foreach ($upn in $UserPrincipalName) {
        Get-MgUser -UserId $upn -Property Id, DisplayName, UserPrincipalName
    }
}
else {
    Write-Host "No UserPrincipalName supplied, pulling all enabled users. This can take a while in a large tenant."
    $users = Get-MgUser -All -Filter "accountEnabled eq true" -Property Id, DisplayName, UserPrincipalName
}

$results = New-Object System.Collections.Generic.List[object]
$total = $users.Count
$counter = 0

foreach ($user in $users) {
    $counter++
    Write-Progress -Activity "Reading To Do task lists" -Status "$($user.UserPrincipalName)" -PercentComplete (($counter / $total) * 100)

    try {
        $lists = Get-MgUserTodoList -UserId $user.Id -All -ErrorAction Stop
    }
    catch {
        Write-Warning "Could not read task lists for $($user.UserPrincipalName): $($_.Exception.Message)"
        continue
    }

    if (-not $lists -or $lists.Count -eq 0) {
        $results.Add([PSCustomObject]@{
            DisplayName        = $user.DisplayName
            UserPrincipalName  = $user.UserPrincipalName
            TaskListName       = "(none)"
            OpenTaskCount      = 0
            CompletedTaskCount = 0
        })
        continue
    }

    foreach ($list in $lists) {
        try {
            $tasks = Get-MgUserTodoListTask -UserId $user.Id -TodoTaskListId $list.Id -All -ErrorAction Stop
        }
        catch {
            Write-Warning "Could not read tasks in list '$($list.DisplayName)' for $($user.UserPrincipalName): $($_.Exception.Message)"
            continue
        }

        $openCount = ($tasks | Where-Object { $_.Status -ne "completed" }).Count
        $completedCount = ($tasks | Where-Object { $_.Status -eq "completed" }).Count

        $results.Add([PSCustomObject]@{
            DisplayName        = $user.DisplayName
            UserPrincipalName  = $user.UserPrincipalName
            TaskListName       = $list.DisplayName
            OpenTaskCount      = $openCount
            CompletedTaskCount = $completedCount
        })
    }
}

Write-Progress -Activity "Reading To Do task lists" -Completed

$results | Sort-Object DisplayName, TaskListName | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8

Write-Host "Report written to $OutputPath ($($results.Count) rows across $($users.Count) users)"

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