How to Check Every Retention Policy in Microsoft 365

Where Microsoft 365 keeps retention settings and how to list every policy, from the Purview portal or with a read-only PowerShell script.

Written by: Justin

Published on: October 26, 2026

Retention policies decide how long Microsoft 365 keeps email, files, Teams chats and Copilot conversations, and when it deletes them. They are easy to set up once and forget. Then someone asks what you keep and for how long, and the honest answer is “I’d have to check.” Here is how to check.

Where retention settings live

Microsoft 365 keeps retention settings in more than one place, which is why nobody remembers them all.

  • Purview retention policies. The main tool. Each one covers whole locations, such as all Exchange mailboxes, some SharePoint sites, Teams chats, or Copilot.
  • Retention label policies. These publish labels that people apply to individual emails and files, or apply labels automatically. They show up in the same PowerShell list as retention policies.
  • Exchange retention policies. The older, mailbox-only kind, also called messaging records management. Exchange Online gives every new mailbox the Default MRM Policy, which moves items older than two years into the archive mailbox when archiving is turned on.
  • Holds. Litigation Hold and eDiscovery holds keep content no matter what a retention policy says. I covered those in How to List Every Mailbox on Litigation Hold.

What you need

  • An account with the View-Only Retention Management role. The Global Reader role includes it, so you don’t need to be a Compliance Administrator just to look. Microsoft Learn lists the roles.
  • For the portal, any web browser.
  • For the full report, a Windows PC with PowerShell 7 and the ExchangeOnlineManagement module. This part needs Windows. The retention cmdlets live in Security & Compliance PowerShell, and Microsoft says it isn’t available in PowerShell 7 on Linux or macOS. I normally run my scripts on Ubuntu, so this one is the exception.
Install-Module ExchangeOnlineManagement -Scope CurrentUser

Check in the Purview portal

This is the quickest way to see what you have, and it works from any computer.

  1. Sign in to the Microsoft Purview portal.
  2. Go to Solutions > Data Lifecycle Management > Policies > Retention policies.
  3. Select each policy. The pane that opens shows the locations it covers, what it keeps or deletes and for how long, and whether it has finished applying.
  4. Check Retention labels and Label policies under Data Lifecycle Management too, if anyone in your organization uses labels.

To see which policies apply to one person, site or group, use Policy lookup in the same area. It needs the exact email address or site URL.

Check with PowerShell

The portal is fine for a handful of policies. For a list you can save, compare later, or hand to an auditor, this script reads every retention policy in the tenant and writes one row per policy to a CSV file. It changes nothing.

#Requires -Modules ExchangeOnlineManagement
<#
.SYNOPSIS
    Lists every Microsoft Purview retention policy in a Microsoft 365 tenant: what each one
    covers, what it does, and whether anything covers Copilot. Read-only.

.DESCRIPTION
    Connects to Security & Compliance PowerShell and reads both families of retention policy:
    the older ones (Get-RetentionCompliancePolicy, which also returns retention label policies)
    and the newer app policies (Get-AppRetentionCompliancePolicy, used for Teams chats, Copilot
    and other AI apps, and Viva Engage). It reads each policy's rule, prints a summary, and saves
    the details to a CSV file. It changes nothing in the tenant.

    Security & Compliance PowerShell works only on Windows. Microsoft doesn't offer it in
    PowerShell 7 on Linux or macOS.

.PARAMETER UserPrincipalName
    The account to sign in with, for example admin@contoso.com. Optional.

.PARAMETER OutputPath
    Where to save the CSV. Defaults to RetentionPolicies.csv in the current folder.

.EXAMPLE
    .\Get-RetentionPolicyReport.ps1 -UserPrincipalName admin@contoso.com

.NOTES
    Needs the ExchangeOnlineManagement module (3.2.0 or later) and an account with the
    View-Only Retention Management role. The Global Reader role group includes it.
#>
[CmdletBinding()]
param(
    [string]$UserPrincipalName,
    [string]$OutputPath = './RetentionPolicies.csv'
)

# Retention actions as a rule stores them, in plain words.
$actionText = @{
    Keep          = 'Retain'
    KeepAndDelete = 'Retain, then delete'
    Delete        = 'Delete'
}

# Location property names, minus "Location", in the words the Purview portal uses.
$locationText = @{
    Exchange      = 'Exchange mailboxes'
    SharePoint    = 'SharePoint sites'
    OneDrive      = 'OneDrive accounts'
    ModernGroup   = 'Microsoft 365 Groups'
    PublicFolder  = 'Exchange public folders'
    Skype         = 'Skype for Business'
    TeamsChat     = 'Teams chats'
    TeamsChannel  = 'Teams channel messages'
    AdaptiveScope = 'Adaptive scopes'
}

function Format-Duration {
    # 2555 becomes "7 years", 30 becomes "30 days".
    param([int]$Days)
    if ($Days -ge 365 -and $Days % 365 -eq 0) {
        $years = $Days / 365
        if ($years -eq 1) { return '1 year' }
        return "$years years"
    }
    if ($Days -eq 1) { return '1 day' }
    return "$Days days"
}

function Get-LocationText {
    # Lists what a policy covers, for example "Exchange: All; OneDrive: 3 selected; Apps: User:M365Copilot".
    param($Policy)
    $parts = @(foreach ($property in $Policy.PSObject.Properties) {
        if ($property.Name -notlike '*Location') { continue }
        $values = @($property.Value | Where-Object { $_ })
        if ($values.Count -eq 0) { continue }
        $names = @(foreach ($value in $values) {
            if ($value.PSObject.Properties['Name'] -and $value.Name) { $value.Name } else { "$value" }
        })
        $label = $property.Name -replace 'Location$', ''
        if ($locationText[$label]) { $label = $locationText[$label] }
        if ($names -contains 'All') { "${label}: All" } else { "${label}: $($names.Count) selected" }
    })
    $apps = @($Policy.Applications | Where-Object { $_ })
    if ($apps.Count -gt 0) { $parts += "Apps: $($apps -join ', ')" }
    $parts -join '; '
}

function Get-RuleText {
    # Describes what one rule does, for example "Retain for 7 years, then delete (counted from creation)".
    param($Rule)
    if ($Rule.PublishComplianceTag) { return "Publishes retention label: $($Rule.PublishComplianceTag)" }
    if ($Rule.ApplyComplianceTag) { return "Auto-applies retention label: $($Rule.ApplyComplianceTag)" }

    $action = "$($Rule.RetentionComplianceAction)"
    $duration = "$($Rule.RetentionDuration)"
    if ($duration -match '^\d+$') {
        $period = Format-Duration -Days ([int]$duration)
        $text = switch ($action) {
            'Keep'          { "Retain for $period" }
            'KeepAndDelete' { "Retain for $period, then delete" }
            'Delete'        { "Delete after $period" }
            default         { "$action, $period" }
        }
    }
    elseif ($action -eq 'Keep' -and ($duration -eq 'Unlimited' -or -not $duration)) {
        $text = 'Retain forever'
    }
    else {
        $text = (@($actionText[$action], $action, 'Unknown action') | Where-Object { $_ })[0]
        if ($duration) { $text += ", $duration" }
    }

    switch ("$($Rule.ExpirationDateOption)") {
        'CreationAgeInDays'     { $text += ' (counted from creation)' }
        'ModificationAgeInDays' { $text += ' (counted from last change)' }
    }
    $text
}

function ConvertTo-ReportRow {
    # Turns one policy and its rules into one row of the report.
    param($Policy, $Rules, [switch]$AppPolicy)
    $rules = @($Rules | Where-Object { $_ })
    $apps = @($Policy.Applications | Where-Object { $_ })
    $olderTeamsChat = -not $AppPolicy -and @($Policy.TeamsChatLocation | Where-Object { $_ }).Count -gt 0

    $type = 'Retention policy'
    if (@($rules | Where-Object { $_.PublishComplianceTag }).Count -gt 0) { $type = 'Retention label policy (published)' }
    elseif (@($rules | Where-Object { $_.ApplyComplianceTag }).Count -gt 0) { $type = 'Retention label policy (auto-apply)' }

    $notes = @()
    if ("$($Policy.Enabled)" -eq 'False') { $notes += 'Turned off.' }
    if ($Policy.Mode -and "$($Policy.Mode)" -ne 'Enforce') { $notes += "Mode is $($Policy.Mode), not Enforce." }
    if ("$($Policy.RestrictiveRetention)" -eq 'True') { $notes += "Preservation Lock is on, so it can't be turned off or shortened." }
    if ($olderTeamsChat) {
        $notes += "Older Teams chats policy. Microsoft treats these as Teams-only, so don't count on it for Copilot."
    }

    $does = 'No rule found'
    if ($rules.Count -gt 0) { $does = (@($rules | ForEach-Object { Get-RuleText -Rule $_ }) -join ' | ') }

    [pscustomobject]@{
        Name           = $Policy.Name
        Type           = $type
        Enabled        = $Policy.Enabled
        Status         = $Policy.DistributionStatus
        Covers         = Get-LocationText -Policy $Policy
        Does           = $does
        CoversCopilot  = @($apps | Where-Object { $_ -match 'Copilot' }).Count -gt 0
        OlderTeamsChat = $olderTeamsChat
        Notes          = $notes -join ' '
    }
}

if ($IsLinux -or $IsMacOS) {
    Write-Warning "Microsoft doesn't offer Security & Compliance PowerShell on Linux or macOS, so the connection will probably fail. Run this on Windows, or check the policies in the Purview portal."
}

$connect = @{ ShowBanner = $false; ErrorAction = 'Stop' }
if ($UserPrincipalName) { $connect.UserPrincipalName = $UserPrincipalName }
Connect-IPPSSession @connect

try {
    $report = [System.Collections.Generic.List[object]]::new()

    # Older retention policies and retention label policies. Locations are accurate only with
    # -DistributionDetail, which Microsoft documents one policy at a time.
    foreach ($summary in @(Get-RetentionCompliancePolicy -ErrorAction Stop)) {
        try {
            $policy = Get-RetentionCompliancePolicy -Identity $summary.Name -DistributionDetail -ErrorAction Stop
            $rules = Get-RetentionComplianceRule -Policy $summary.Name -ErrorAction Stop
            $report.Add((ConvertTo-ReportRow -Policy $policy -Rules $rules))
        }
        catch {
            Write-Warning "Couldn't read the policy '$($summary.Name)': $($_.Exception.Message)"
        }
    }

    # Newer app policies: Teams chats, Copilot and AI apps, Viva Engage, Teams call logs.
    try {
        $appPolicies = @(Get-AppRetentionCompliancePolicy -ErrorAction Stop)
    }
    catch {
        Write-Warning "Couldn't list app retention policies: $($_.Exception.Message)"
        $appPolicies = @()
    }
    foreach ($summary in $appPolicies) {
        if ($report.Name -contains $summary.Name) { continue }
        try {
            $policy = Get-AppRetentionCompliancePolicy -Identity $summary.Name -DistributionDetail -ErrorAction Stop
            $rules = Get-AppRetentionComplianceRule -Policy $summary.Name -ErrorAction Stop
            $report.Add((ConvertTo-ReportRow -Policy $policy -Rules $rules -AppPolicy))
        }
        catch {
            Write-Warning "Couldn't read the policy '$($summary.Name)': $($_.Exception.Message)"
        }
    }
}
finally {
    Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
}

if ($report.Count -eq 0) {
    Write-Host 'No retention policies were found in this tenant.'
    return
}

$report | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8
$report | Format-Table Name, Type, Status, Does -Wrap | Out-String -Width 200 | Write-Host
Write-Host "Found $($report.Count) policies. Full details, including what each one covers, are in $OutputPath"

# Last, say whether anything covers Copilot. Older Teams chats policies don't count.
$copilot = @($report | Where-Object CoversCopilot)
$olderTeams = @($report | Where-Object OlderTeamsChat)
if ($copilot.Count -gt 0) {
    Write-Host "Copilot prompts and responses are covered by: $($copilot.Name -join ', ')"
}
elseif ($olderTeams.Count -gt 0) {
    Write-Host "No policy covers Copilot prompts and responses. Older Teams chats policies ($($olderTeams.Name -join ', ')) don't count, because Microsoft treats them as Teams-only."
}
else {
    Write-Host 'No retention policy covers Copilot prompts and responses.'
}

Save it as Get-RetentionPolicyReport.ps1 and run it on Windows with your admin account. A browser window opens for you to sign in.

.\Get-RetentionPolicyReport.ps1 -UserPrincipalName admin@contoso.com

What it does

  • Reads two lists of policies, not one. Microsoft is moving retention to a newer set of cmdlets, so policies for Teams chats, Copilot and other AI apps, and Viva Engage come from Get-AppRetentionCompliancePolicy, while policies for Exchange, SharePoint and OneDrive, and label policies, come from Get-RetentionCompliancePolicy. Check only one and you miss part of the picture.
  • Reads each policy with -DistributionDetail. Without it, Microsoft says, the location values “will be inaccurate.”
  • Describes each rule in plain words, such as “Retain for 7 years, then delete (counted from creation).”
  • Notes any policy that is turned off, not enforced, or under Preservation Lock, which means no one can turn it off or shorten it.
  • Saves everything to RetentionPolicies.csv, then tells you whether anything covers Copilot.

Exchange’s own retention policies

The older Exchange retention policies live in Exchange Online PowerShell, and that one does run on Linux. On a machine without a browser, add -Device to the connect command and sign in from another computer. Microsoft Learn explains the options.

Connect-ExchangeOnline -ShowBanner:$false

# Exchange retention policies, and the tags each one uses
Get-RetentionPolicy | Format-List Name, RetentionPolicyTagLinks

# What each tag does
Get-RetentionPolicyTag | Format-Table Name, Type, AgeLimitForRetention, RetentionAction

# How many mailboxes use each policy
Get-Mailbox -ResultSize Unlimited | Group-Object RetentionPolicy -NoElement | Sort-Object Count -Descending

# Organization-wide Purview retention policies that reach Exchange
Get-OrganizationConfig | Select-Object -ExpandProperty InPlaceHolds

Disconnect-ExchangeOnline -Confirm:$false

The last command lists organization-wide Purview retention policies that reach Exchange. Entries that start with mbx cover mailboxes and older Teams chats policies, and entries that start with grp cover Microsoft 365 Groups and Teams channel messages. The number after the colon says what the policy does: 1 deletes or publishes labels, 2 retains, and 3 retains and then deletes. You get an ID, not a name, so match it up in the portal. Policies for Copilot and the other newer locations don’t show up here at all, which is one more reason to use the portal or the script.

Things to know

  • When policies disagree, retention wins over deletion and the longest retention period wins. A short delete policy doesn’t override a longer one that keeps the same content.
  • A new or changed policy can take up to seven days to apply.
  • Retention policies come with Microsoft 365 Business Premium, E3 and E5, among others. The service description has the full list.
  • The script only reads settings. It doesn’t change any policy, and it disconnects when it finishes.

Once you know what you have, write it down: what each policy covers, how long it keeps things, and why. The next time someone asks, the answer takes a minute instead of an afternoon.

Leave a Comment

Previous

How to Find Inactive Licensed Users in Microsoft 365