Activity Report for Microsoft 365 Groups and Teams

Find Obsolete Groups and Teams

May 11: Microsoft is deprecating the TechNet Gallery in June 2020. The script can now be downloaded from GitHub.

I wrote the first version of a script to analyze the activity in Office 365 Groups and Teams to identify obsolete groups in 2017. The script is described in this Petri.com article and is reasonably popular. I keep an eye on the feedback from people who run the script and update the script as time goes by. You can download the latest version from GitHub. The latest version is V4.5 dated 15 May 2020.

The basic idea is to analyze the Microsoft 365 Groups in a tenant to find underused groups or simply understand the level of activity across groups. The script looks at the level of activity in:

  • Conversations stored in the Inbox of group mailboxes (for Outlook Groups).
  • Documents (in SharePoint document libraries belonging to the groups)
  • Chats (for Teams-enabled Groups). In fact, the script checks the compliance records logged in the group mailbox for conversations in channels belonging to the Teams.

Checking different aspects of individual groups isn’t fast. One tenant tells me that it takes 17 hours to process 5,300 groups… but this isn’t a report that you’ll run daily. It’s more like a monthly or quarterly activity.

Script Outputs

The script records the data for each group in a PowerShell list. Eventually, after processing all the groups, the script outputs an HTML report and a CSV file. The script also assigns a status of Pass, Warning, or Fail to each group depending on the level of detected activity. The determination of the status is entirely arbitrary and should be adjusted to meet the needs of your organization.

The output from the Microsoft 365 Groups and Teams activity report
Figure 1: The output from the Microsoft 365 Groups and Teams activity report

At the bottom of the report you’ll see a summary like this:

Reviewing the report should help you find the Microsoft 365 Groups and Teams that are not being used. These groups are candidates for removal or archival.

You can view a screen capture video showing how to run the script here.

Recent Improvements

Scripts that evolve over time can often do with a rewrite from scratch. However, I don’t have the time for that. but I do make changes that I think are useful. Here are some recent changes.

  • New tests to see if the SharePoint Online and Teams modules are loaded. In particular, the Teams check took far too long because the cmdlets in this module are slow. In fact, the Get-Team module is so slow that we don’t use it from V4.3 onwards. Using Get-UnifiedGroup with a filter is about twice as fast. We might revisit this point as new versions of the Teams PowerShell module appear.
  • Use a PowerShell list object to store the report data. This is much faster than an array, especially when you might want to store data for thousands of groups. This was one of the performance tips received after publishing a post about how we write PowerShell and it makes a real difference.
  • Use Get-Recipient instead of Get-UnifiedGroup to create a set of group objects to process. Get-Recipient is much faster than Get-UnifiedGroup when you need to create a list of mail-enabled objects like Office 365 Groups.
  • Output the storage consumed by the SharePoint site belonging to each group.
  • Handle groups that have no conversations in the group mailbox more elegantly. Groups used by Teams are often in this situation.

The new Exchange Online REST-based cmdlets are not used (yet). It would be easy to replace Get-Recipient with Get-ExoRecipient and Get-MailboxFolderStatistics with Get-ExoMailboxFolderStatistics. I decided to stay with the standard cmdlets because not everyone has yet moved to the new cmdlets. However, a large environment, you might get some extra performance by using Get-ExoRecipient. There is no REST-based version of Get-UnifiedGroup.

Those who manage groups in large environments could also improve processing by using the techniques explained in this EHLO blog post.

Test Before Deployment

As always, test any PowerShell code downloaded from the web, even from sources like GitHub, before introducing it to a production system. The code as written needs some extra error handling to make it as robust as it could be, but I’ve left that to the professionals as people tend to have their own way of approaching this issue.


The Office 365 for IT Pros eBook includes many valuable tips for writing PowerShell scripts to interact with Office 365 Groups and Teams. Subscribe to gain benefit from all that knowledge!

5 Replies to “Activity Report for Microsoft 365 Groups and Teams”

  1. Hello all

    am getting the below error and CSV and HTML file is null

    Results
    ——-
    Number of groups scanned : 0
    Potentially obsolete groups (based on document library activity): 0
    Potentially obsolete groups (based on conversation activity) : 0
    Number of Teams-enabled groups : 9
    You cannot call a method on a null-valued expression.
    At C:\Users\raarumug\Desktop\MS Teams\Scripts\TeamsGroupsActivityReport4.3.PS1:216 char:1
    + Write-Host “Percentage of Teams-enabled groups …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    Summary report in c:\temp\GroupsActivityReport.html and CSV in c:\temp\GroupsActivityReport.csv
    PS C:\Users\raarumug\Desktop\MS Teams\Scripts>

    1. Well, the script hasn’t processed any groups for some reason. What’s in the $TeamList and $Groups variables?

      And to make sure that we don’t run into the same problem, I updated the script to V4.4 to include a check that some groups are found before we proceed.

  2. Great script! Have used it regularly to get an idea of the current status of our 3,500+ groups.

    I noticed though in the last report that I had a handful of groups reporting SPOStatus as ‘Document library never created’ but SPOActivity was ‘Document library in use’ – which didn’t make a lot of sense!

    I found that the $AuditRecs parameter is not set if the $G.SharePointSiteURL does not exist. Thus when it gets set in a loop iteration, on the next iteration the previous value is carried over if $G.SharePointSiteURL does not exist.

    I’ve fixed this in my copy of the script, just thought you’d like to know!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.