How to Report Groups Under the Control of the Microsoft 365 Groups Expiration Policy

Yammer Communities Now Covered by Groups Expiration Policy

Updated 24 April 2023

A reader question about the Microsoft 365 Groups expiration policy caused me to review some PowerShell code I wrote to report the next renewal dates for the set of groups within the scope of the expiration policy. The question was related to Yammer (now Viva Engage), to know if the Microsoft 365 Group expiration policy covers the groups by Yammer and will remove inactive groups when necessary. The answer is yes; Microsoft updated policy processing last year to accommodate the Microsoft 365 groups used by Yammer communities when networks run in Microsoft 365 native mode. Microsoft confirmed coverage for Yammer communities by the groups expiration policy in MC324202 (published today). Microsoft 365 roadmap item 82186 also deals with the scenario and says that general availability occurred in January 2022.

In 2020, Microsoft changed the way the Microsoft 365 Groups expiration policy works to introduce automatic renewal. Instead of bothering group owners with email to remind them to renew the group, a background job looks for evidence that the group is active. If the evidence exists, Microsoft 365 renews the group automatically. Unfortunately, a limited set of signals govern renewal:

  • SharePoint Online: View, edit, download, move, share, or upload files.
  • Outlook: Join group, read/write message in the group mailbox, or like a message (in OWA).
  • Teams: Visit a Teams channel.
  • Yammer: View a post within a Yammer community or an interactive email in Outlook.

It’s debatable if a group is active if just one group member visits a Teams channel or views a post in a Yammer community. The Microsoft Graph gathers a wide array of signals about user activity and there’s surely a more precise method to determine group activity than the actions cited above. The Groups and Teams activity report is an example of how to code your own assessment of group activity.

In any case, the answer remains that you can add the Microsoft 365 groups used by Viva Engage communities to the Groups expiration policy through the Microsoft Entra admin center (Figure 1).

Figure 1: Defining groups to include in the expiration policy

You can also add groups to the policy with PowerShell.

The Groups expiration policy is appliable to selected groups or to all Microsoft 365 groups in the tenant. Users who are members of the groups covered by the policy must have Azure AD Premium P1 licenses.

PowerShell Code to Report Group Expiration

Writing code to report the expiration dates for groups isn’t difficult. The date when the group needs to be next renewed is in the ExpirationTime property. The only complication is to find when the group was last renewed. This data isn’t returned by the Get-UnifiedGroup cmdlet, so we need to use the Get-AzureADMSGroup cmdlet. Once we know where to get the dates, we can report what we find. This code runs after connecting to the Exchange Online and Azure AD PowerShell modules.

Write-Host "Finding Microsoft 365 Groups to check…"
[array]$ExpirationPolicyGroups  = (Get-UnifiedGroup -ResultSize Unlimited | ? {$_.ExpirationTime -ne $Null} | Select DisplayName, ExternalDirectoryObjectId, WhenCreated, ExpirationTime )
If (!($ExpirationPolicyGroups)) { Write-Host "No groups found subject to the expiration policy - exiting" ; break }
Write-Host $ExpirationPolicyGroups.Count “groups found. Now checking expiration status.”
$Report = [System.Collections.Generic.List[Object]]::new(); $Today = (Get-Date)
ForEach ($G in $ExpirationPolicyGroups) {
        $Days = (New-TimeSpan -Start $G.WhenCreated -End $Today).Days  # Age of group
        $LastRenewed = (Get-AzureADMSGroup -Id $G.ExternalDirectoryObjectId).RenewedDateTime
        $DaysLeft = (New-TimeSpan -Start $Today -End $G.ExpirationTime).Days
        $ReportLine = [PSCustomObject]@{
           Group       = $G.DisplayName
           Created     = Get-Date($G.WhenCreated) -format g
           AgeinDays   = $Days
           LastRenewed = Get-Date($LastRenewed) -format g
           NextRenewal = Get-Date($G.ExpirationTime) -format g
           DaysLeft    = $DaysLeft}
          $Report.Add($ReportLine)
} # End Foreach
CLS;Write-Host "Total Microsoft 365 Groups covered by expiration policy:" $ExpirationPolicyGroups.Count
Write-Host “”
$Report | Sort DaysLeft | Select Group, @{n="Last Renewed"; e= {$_.LastRenewed}}, @{n="Next Renewal Due"; e={$_.NextRenewal}}, @{n="Days before Expiration"; e={$_.DaysLeft}}

Total Microsoft 365 Groups covered by expiration policy: 74

Group                                Last Renewed     Next Renewal Due Days before Expiration
-----                                ------------     ---------------- ----------------------
Potholers (Team)                     02/02/2020 07:16 21/02/2022 07:16                     13
Office 365 Questions                 19/05/2017 11:12 14/03/2022 15:04                     34
Corona Virus News                    10/03/2020 21:56 30/03/2022 22:56                     51
Contract Workers                     12/03/2020 08:57 01/04/2022 09:57                     52
Plastic Production (Team)            25/03/2020 08:48 14/04/2022 09:48                     65

The code works, with two caveats:

  1. Get-UnifiedGroup is not a fast cmdlet. It’s OK to run it against a couple of hundred groups, but once that number grows, the time needed for the cmdlet to retrieve details of the groups to process gets longer and longer.
  2. The Get-AzureADMSGroup cmdlet is affected by Microsoft’s decision to retire the Azure AD module. Although the cmdlet will continue to run after June 30, 2023, you don’t know when it will cease functioning.

The solution for both speed and supportability is to use a Microsoft Graph API query to fetch group details.

Using the Graph to Fetch Microsoft 365 Groups to Report Expiration Details

Essentially, what we need to do is to replace the call to Get-UnifiedGroup with a Graph API query to return the set of groups in the tenant. The bonus is that the query returns the last renewed time, so there’s no need to use Get-AzureADMSGroup.

As with any script that calls Graph queries from PowerShell, you need a registered application in Azure AD to hold the permissions required to run the queries used by the script. In this case, we only need the Group.Read.All permission. After securing an access token, we can fetch the set of groups in the tenant using a lambda filter. The code shown below uses a function (Get-GraphData) to execute the Invoke-RestMethod cmdlet to fetch the data and page until all groups are retrieved. You can see the code for the Get-GraphData function in this script.

After fetching the set of groups, we create a report detailing the group name, its creation date, the date last renewed, and expiration date. The code used to process the data returned by Get-UnifiedGroup is modified to deal with the property names returned by the Graph query.

$uri = "https://graph.microsoft.com/beta/groups?`$filter=ExpirationDateTime ge 2014-01-01T00:00:00Z AND groupTypes/any(a:a eq 'unified')&`$count=true" 
[array]$Groups = Get-GraphData -AccessToken $Token -Uri $uri
If (!($Groups)) { Write-Host "No groups found subject to the expiration policy - exiting" ; break }
$Report = [System.Collections.Generic.List[Object]]::new(); $Today = (Get-Date)
ForEach ($G in $Groups) {
        $Days = (New-TimeSpan -Start $G.CreatedDateTime -End $Today).Days  # Age of group
        #$LastRenewed = $G.RenewedDateTime
        #$NextRenewalDue = $G.ExpirationDateTime
        $DaysLeft = (New-TimeSpan -Start $Today -End $G.ExpirationDateTime).Days
        $GroupsInPolicy++
        $ReportLine = [PSCustomObject]@{
           Group                   = $G.DisplayName
           Created                 = Get-Date($G.CreatedDateTime) -format g
          "Age in days"            = $Days
          "Last renewed"           = Get-Date($G.RenewedDateTime) -format g
          "Next renewal"           = Get-Date($G.ExpirationDateTime) -format g
          "Days before expiration" = $DaysLeft}
          $Report.Add($ReportLine)
} # End ForeachCLS;Write-Host "Total Microsoft 365 Groups covered by expiration policy:" $Groups.Count
Write-Host “”
$Report | Sort "Days before expiration"| Select Group, "Last renewed", "Next renewal", "Days before expiration" | Out-GridView

As you’d expect, things run much faster. Retrieving data through a Graph query is always quicker than using a PowerShell cmdlet and eliminating the call to Get-AzureADMSGroup for each group helps speed things up even further. Figure 2 shows the output.

Expiration dates for Microsoft 365 Groups

Microsoft 365 Groups Expiration policy
Figure 2: Expiration dates for Microsoft 365 Groups

An even easier solution is to replace the calls to Get-UnifiedGroup and Get-AzureADMSGroup with the Get-MgGroup cmdlet from the Microsoft Graph PowerShell SDK. Here’s the code, which is almost as fast as using the Graph API:

Write-Host "Finding Microsoft 365 Groups to check…"
[array]$ExpirationPolicyGroups = Get-MgGroup -Filter "groupTypes/any(c:c eq 'unified')" -All | ? {$_.ExpirationDateTime -ne $Null }
If (!($ExpirationPolicyGroups)) { Write-Host "No groups found subject to the expiration policy - exiting" ; break }
Write-Host $ExpirationPolicyGroups.Count “groups found. Now checking expiration status.”
$Report = [System.Collections.Generic.List[Object]]::new(); $Today = (Get-Date)
ForEach ($G in $ExpirationPolicyGroups) {
        $Days = (New-TimeSpan -Start $G.CreatedDateTime -End $Today).Days  # Age of group
        $DaysLeft = (New-TimeSpan -Start $Today -End $G.ExpirationDateTime).Days
        $ReportLine = [PSCustomObject]@{
           Group       = $G.DisplayName
           Created     = Get-Date($G.CreatedDateTime) -format g
           AgeinDays   = $Days
           LastRenewed = Get-Date($G.RenewedDateTime) -format g
           NextRenewal = Get-Date($G.ExpirationDateTime) -format g
           DaysLeft    = $DaysLeft}
          $Report.Add($ReportLine)
} # End Foreach
CLS;Write-Host "Total Microsoft 365 Groups covered by expiration policy:" $ExpirationPolicyGroups.Count
Write-Host “”
$Report | Sort DaysLeft | Select Group, @{n="Last Renewed"; e= {$_.LastRenewed}}, @{n="Next Renewal Due"; e={$_.NextRenewal}}, @{n="Days before Expiration"; e={$_.DaysLeft}}

Speeding Up Queries

The question about Yammer communities forced me to look at code and find an instance where I needed to replace a cmdlet before its deprecation next June. At the same time, I managed to speed up the code by introducing a Graph query. Things worked out for the best, but it does illustrate the need to check and update old scripts on an ongoing basis.


Learn how to exploit the data available to Microsoft 365 tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.

10 Replies to “How to Report Groups Under the Control of the Microsoft 365 Groups Expiration Policy”

  1. Hi ,

    I just want to know if the Autorenewal activity through Yammer that was introduced in Jan 2022 is applicable for a network that is non native. Yet the groups are M365 groups

  2. Hi Toni,
    it’s a bit trial and error, because there is no mentioning to which cloud services I should connect (Exchange, AzureAD, etc.)
    Thinking broadly about Graph, is there a possibility to create a report / query on Azure admin portal without PowerShell, e.g. a Workbook based on Kusto query, or similar?

  3. Thank you for your post… I am just wondering if A planner activity counts as an activity for the group expiration..

  4. Does this example require group expiration policy to be enabled on the tenant? Or can it be run as a ‘what-if’ prior to enabling group expiration? If group expiration needs to be on for this to work, is there an alternative method that could provide the expiration information based on an expiration timeframe I provide? For example, what is ‘days to expiration’ if I set group lifetime to 365 days?

    1. Expiration policies depend on groups being stamped with an expiration date and Graph signals. You can mimic the date check by using a custom attribute to store an expiration date but it’s hard to mimic the Graph signals.

Leave a Reply

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