Blog Post

Planner Blog
5 MIN READ

How do I find all my Planner plans?

Brian-Smith's avatar
Brian-Smith
Microsoft
Mar 11, 2021

One question we often get in support is, “How do I find all my Planner plans?” The answer to this is not too tricky—the tricky bit is ‘all plans; rather than ‘all my plans’—as, due to security and privacy controls, you can only ever see plans that are in a group where you are a member (and being an owner does not automatically mean you are a plan member as far as Planner is concerned). But with that constraint how can I get a list of all my plans other than looking at the Planner hub? PowerShell and the Microsoft Graph API are one way.

An animation showing some plans, the PowerShell code and then the output list of groups and plans

I’ve posted previously about using the Graph with Planner, and to avoid repeating some of the basics I won’t cover the creation of App Registrations in Azure Active Directory in depth. You can find those details in this blog from Lee Ford. His blog is not about Planner specifically, but has very good steps on the App Registration using a more recent UI than I did in my earlier posts. I’ll note in the PowerShell code walkthrough below the specific permissions needed. 

I also use the latest MSAL authentication, using the MSAL.PS module to make things easier. You'll need to have the most up-to-date version of PowerShell and preferably PowerShellGet to make it easier to install modules. I usually work in the ISE, too. On to the code!

First, I have a function that helps to handle failed web callsunless you get a 200 http response, it will be handled as an exceptionso you need to use Try/Catch rather than just read the status of the response. I found this useful function on Chris Wahl’s blog.

 

 

 

# Script to iterate Groups and get all Plans

function not200 {
$global:result = $_.Exception.Response.GetResponseStream()
$global:reader = New-Object System.IO.StreamReader($global:result)
$global:responseBody = $global:reader.ReadToEnd();
$message = $global:responseBody | ConvertFrom-Json
Write-Host -BackgroundColor:Black -ForegroundColor:Red "Status: A system exception was caught."
Write-Host -BackgroundColor:Black -ForegroundColor:Red $message.error.message
} 

 

 

 

This will be called in the catch to give the specific error (usually it will be a “403" for Groups that you read, but since you’re not a member you can’t see the groups). You could use this information to either find a member or add yourself to check for plans.

Next is the authentication using MSAL. First, you need to install the module either manually or using the Install-Module if you have PowerShellGet.

 

 

 

# MSAL.PS added to the function to support the MSAL libraries
# Available from https://github.com/AzureAD/MSAL.PS or https://www.powershellgallery.com/packages/MSAL.PS
# Or Install-Module MSAL.PS -AcceptLicense
Import-Module MSAL.PS

# Interactive login
# Client ID is created in Azure AD under App Registration - requires Group.Read.All and the default User.Read
# Redirect Url is Mobile and Desktop applications - https://login.microsoftonline.com/common/oauth2/nativeclient
# Change TenantId to your own tenant 

 

 

 

For the call, you need to have your TenantId as well as a ClientId that you have configured via Azure AD as per the reference I shared above. This assumes you are making an interactive loginso it will prompt youbut if necessary you could configure with any of the supported MSAL authentication flows. Just a reminder, Planner always requires an app + user authentication.

 

 

 

$graphToken = Get-MsalToken -ClientId "<replace with your client id>" -TenantId "<replace with your tenant id>" `
-Interactive -Scope 'https://graph.microsoft.com/Group.Read.All', 'https://graph.microsoft.com/User.Read' `
-LoginHint <use your fully qualified domain name as login hint> 

 

 

 

Once you have the token, the next section is where the work happens, as it makes a call to get all groups and then iterates over the groups to get the plans in each groupassuming you have access.  As you can potentially have a lot of groups, the response can be paged, so the while loop handles this seamlessly to get all groups in one PowerShell object. There are very useful Graph examples in this blog from Rezwanur Rahman. I used the top 10 filter to give me a nextlinkeven though I don’t have that many groupsand to check the while" functionality. This also shows how the parameters are passed in, which are not really the body of the call but work as url query strings. I also use the parameter to pull just two fields.

 

 

 

#################################################
# Get Groups
#################################################

$headers = @{}
$headers.Add('Authorization','Bearer ' + $graphToken.AccessToken)
$headers.Add('Content-Type', "application/json")

$parameters = @{'$select'='id,displayname'
'$top'='10'} 

$uri = "https://graph.microsoft.com/v1.0/groups"

# Fetch all groups

$groupsRequest = Invoke-RestMethod -Uri $uri -Method GET -Headers $headers -Body $parameters

$groups = @()
$groups+=$groupsRequest.value

while($groupsRequest.'@odata.nextLink' -ne $null){
    $groupsRequest = Invoke-RestMethod -Uri $groupsRequest.'@odata.nextLink' -Method GET -Headers $headers
    $groups+=$groupsRequest.value
    }  

 

 

 

Now that I have all my groups, I’ll next look at each for any plans.

 

 

 

# Iterate through the Groups and find the plans owned by each group

ForEach($value in $groups)
{
    Write-host " "
    Write-host "Group ID           = " $value.id
    Write-host "Group Display Name = " $value.displayname

    $parameters = @{'$select'='id,title'}
    
    $uri = "https://graph.microsoft.com/v1.0/groups/" + $value.id + "/planner/plans"
    try
    # This code will read all groups - but if you are not a member it may fail to read plans 
    {

        $plans = Invoke-WebRequest -Uri $uri -Method Get -Headers $headers -Body $parameters -UseBasicParsing
            if($plans.StatusCode -eq '200')
            {
                $plansContent = $plans.Content | ConvertFrom-Json
                $plansContentValues = $plansContent.value
                ForEach($value in $plansContentValues)
                {
                    Write-host "     Plan ID    = "$value.id
                    Write-host "     Plan title = "$value.title
                }
            }
            else
            {
                Write-host "Likely permissions issue with " $value.displayname "site/plan"
            }

     }catch
        {
            not200 
            Write-host "     No access to group - "$value.description " with the /planner/plans call"
            Write-host "     Check in AAD or Graph" 
        } # avoids the 403 displaying - may hide other failures
        
} 

 

 

 

Again, I am using parameters to get the fields I want. I also found that on some machines I didn’t need the -UseBasicParsing flagthat depends on exact versions and is now the default. The output of this script will show the group ID and display name, and then indented underneath the plan ID and title for each plan.

Part of the output showing groups and plans and a failure

I hope you find this useful.

Updated Mar 11, 2021
Version 1.0