Easily Done, But Prepare for Teams That Expire Afterwards
A contribution to the Microsoft Technical Community offered a solution for how to add all the Teams in an Office 365 tenant to the Office 365 Groups Expiration Policy. Once teams are added to the policy, the groups they belong to expire at the end of the policy lifetime (say, 750 days) and must be renewed by a team owner. If not, the group is soft-deleted for 30 days, during which time it can be recovered. At the end of that period, Office 365 permanently removes the group for the team and all the associated resources.
In any case, the script worked on the basis of finding all the Office 365 Groups in the tenant and then figuring out which groups are team-enabled before adding those groups to the policy. It’s a valid approach, but a better method is to use the Get-Team cmdlet in the Teams PowerShell module because it only returns the set of teams and you don’t have to fiddle around checking what groups are team-enabled.
Once you have the set of teams, it’s easy to add them to the expiration policy using the Add-AzureADMSLifecyclePolicyGroup cmdlet.
Tuning The Solution
A script showing how to add multiple groups to the expiration policy is included in the chapter covering how to manage Groups and Teams in the Office 365 for IT Pros eBook. It was simple to take that script and amend it to process Teams rather than Groups. We can improve the solution further by implementing a check for teams already covered by the policy so we don’t trigger an error when running Add-AzureADMSLifecyclePolicyGroup.
One way to do this is to update one of the fifteen custom attributes available for all mail-enabled objects. In the example, we write the GUID of the policy into this attribute, meaning that we can check for its existence before trying to add a team to the policy. Here’s the code:
# Add all Teams that aren't already covered by the Groups expiration policy
# to the policy
$PolicyId = (Get-AzureADMSGroupLifecyclePolicy).Id
$TeamsCount = 0
Write-Host "Fetching list of Teams in the tenant…"
$Teams = Get-Team
ForEach ($Team in $Teams) {
$CheckPolicy = (Get-UnifiedGroup -Identity $Team.GroupId).CustomAttribute3
If ($CheckPolicy -eq $PolicyId) {
Write-Host "Team" $Team.DisplayName "is already covered by the expiration policy" }
Else {
Write-Host "Adding team" $Team.DisplayName "to group expiration policy"
Add-AzureADMSLifecyclePolicyGroup -GroupId $Team.GroupId -Id $PolicyId -ErrorAction SilentlyContinue
Set-UnifiedGroup -Identity $Team.GroupId -CustomAttribute3 $PolicyId
$TeamsCount++ }}
Write-Host "All done." $TeamsCount "teams added to policy"
Another advantage of using a custom attribute is that many cmdlets support server-side filtering for this attributes. For instance, to find the set of teams that have been added to the expiration policy, we can run the command:
After I wrote the original post, Microsoft updated the Groups expiration policy to be activity based. A side effect of the change was that the Get-UnifiedGroup cmdlet returns the calculated expiration date for groups covered by the policy, meaning that you could use this instead of a custom attribute to figure out what groups are covered by the policy. Thus, we can now base the script on the expiration date as shown below.
$PolicyId = (Get-AzureADMSGroupLifecyclePolicy).Id
Write-Host "Fetching list of Teams in the tenant…"
[array]$Teams = Get-Team
$TeamsCount = 0
ForEach ($Team in $Teams) {
$CheckPolicy = $Null
$CheckPolicy = (Get-UnifiedGroup -Identity $Team.GroupId).ExpirationTime
If ($CheckPolicy -ne $Null) {
Write-Host "Team" $Team.DisplayName "covered by expiration policy and will expire on" (Get-Date($CheckPolicy) -format g)}
Else {
Write-Host "Adding team" $Team.DisplayName "to group expiration policy" -Foregroundcolor Red
Add-AzureADMSLifecyclePolicyGroup -GroupId $Team.GroupId -Id $PolicyId -ErrorAction SilentlyContinue
Set-UnifiedGroup -Identity $Team.GroupId -CustomAttribute3 $PolicyId
$TeamsCount++ }}
Write-Host "All done." $TeamsCount "Teams added to policy"
In turn, this means that to find the groups covered by the policy you can do the following (unfortunately the ExpirationTime property is not supported for server-side filtering):
After you add a bunch of groups to the expiration policy, the likelihood exists that some of those groups will expire because they are already older than the expiration period. For this reason, it’s a good idea to prepare team owners to let them know what to do if they see an expiration notice. If the group is team-enabled, the notification appears in the activity feed of the team owner (Figure 1).
Figure 1: Expiring teams show up in the team owner’s activity feed
They can then extend the lifetime of the team by editing its settings. Select the team expiration option to view the current expiration date and then click Renew now (Figure 2) if needed.
Figure 2: Renewing an expired group via Teams settings
Need more examples of how to manage Teams and Office 365 Groups (and many other things) with PowerShell? Look no further than the Office 365 for IT Pros eBook. At the last count, the text included over a thousand examples.
{"id":null,"mode":"button","open_style":"in_modal","currency_code":"EUR","currency_symbol":"\u20ac","currency_type":"decimal","blank_flag_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":100,"top_media_type":"featured_image","featured_image_url":"https:\/\/office365itpros.com\/wp-content\/uploads\/2022\/11\/cover-141x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Virtual Tip Jar","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"Office 365 for IT Pros","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"Office 365 for IT Pros","form_subtitle":null,"currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for supporting the work of Office 365 for IT Pros!","payment_confirmation_title":"Office 365 for IT Pros","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
That looks good Tony. It is much less work this way.
Thanks
Aldo (@Jan Klaasen)
Why not use Get-UnifiedGroup | select ExpirationTime instead of managing your own attribute?
Because that attribute wasn’t available with Get-UnifiedGroup when the post was written last year…
Thank you
Updated the post to use ExpirationTime…
Thanks, I had been using Graph API to pull in the expiration date, didn’t catch the new ExpirationTime Attribute