How do I find all my Planner plans?

Published 03-11-2021 06:35 AM 2,220 Views
Frequent Contributor

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 plansAn 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 failurePart of the output showing groups and plans and a failure

I hope you find this useful.

6 Comments
Occasional Contributor

“How do I find all my Planner plans?” 

should read “How do I find all my Planner plans as a member of the technical IT team?” because while its great that you can query Graph API etc with PowerShell, for the remaining 99% of our user base this solution is a complete non starter 

Occasional Visitor

Thank you for taking the time to make this post and explain the technical side. As AndyTuke eluded, this is too technical for most users (including me), but I do appreciate the effort and to know that people at Microsoft are looking into ways to improve Planner.  Keep up the good work, and I look forward to continued updates and improvements for Planner. All the best!

New Contributor

@Brian Smith  Hello, Thank you for putting this together! Was the "not200" added in error" (code snippet below)  Once i removed that, i stopped getting red text.  

{
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


not200 : The term 'not200' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At D:\Powershell\Planner_Find_Planner_Plans.ps1:64 char:13
+ not200
+ ~~~~~~
+ CategoryInfo : ObjectNotFound: (not200:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException

Frequent Contributor

Hi @Joseph Halpy - that line calls the function when the status response was not an "OK" or code 200, hence the name not200.  If you remove that then you would not get the red text, but you wouldn't know that there are groups you are not a member of that 'may' have plans in them.  If the not200 is not recognized then likely you did not run the earlier part of the script that defines the function 'not200'.

I hope this helps,

Brian

Frequent Contributor

Thanks @AndyTuke and @POWEREH , and I do realize that this level of coding isn't for everyone.  The Planner team are looking to get more info into the admin center to show usage of Planner so hopefully that will help.  The question more often does come from the IT teams, who don't have a real idea on the usage of Planner in their organizations.  For non IT team users then looking in the Planner hub may be the best place. 

Best regards,

Brian

New Contributor

@Brian Smith Thanks Brian, yes that was it.  Able to add the code to export to csv?  

Co-Authors
Version history
Last update:
‎Mar 11 2021 06:35 AM
Updated by: