Get All Inactive Microsoft Teams with PowerShell – A Quick Guide


Teams sprawl is a real problem in any tenant that’s been running a while — project Teams that outlived the project, one-off Teams nobody remembers creating, Teams with external guests still sitting in them long after anyone used the channel. Finding which ones are actually dead (not just quiet) is the first step before archiving or deleting anything.


In this post: Prerequisites · Pulling the activity report · Prioritizing by guest exposure, not just silence · When the report comes back anonymized · Best practices · When would you actually use this? · Related reading


Prerequisites
  • At least the Reports Reader role in Microsoft 365 — you don’t need Global Admin just to pull activity reports.
  • The Microsoft.Graph.Reports PowerShell module installed, connected with the Reports.Read.All scope.
  • Audit logging matters less here than you’d think — the Teams activity report is a separate, purpose-built dataset, not derived from the unified audit log (which has its own, shorter retention window).

Pulling the activity report

Microsoft Graph has a purpose-built report for exactly this — Get-MgReportTeamActivityDetail returns a per-team CSV with a Last Activity Date column, so you’re not reconstructing activity from raw audit log entries:

# Install Microsoft Graph Reports module if not already installed
Install-Module Microsoft.Graph.Reports -Scope CurrentUser

# Connect with reporting scope
Connect-MgGraph -Scopes "Reports.Read.All"

# Pull the team activity report -- period options are D7, D30, D90, D180
$reportPath = "$env:TEMP\TeamsActivity.csv"
Get-MgReportTeamActivityDetail -Period "D90" -OutFile $reportPath

# Load it and find teams with no activity inside the window
$report = Import-Csv -Path $reportPath
$inactiveThresholdDays = 90
$cutoffDate = (Get-Date).AddDays(-$inactiveThresholdDays)

$inactiveTeams = $report | Where-Object {
    [string]::IsNullOrEmpty($_.'Last Activity Date') -or
    [datetime]$_.'Last Activity Date' -lt $cutoffDate
}

$inactiveTeams | Select-Object 'Team Name', 'Team Id', 'Last Activity Date' |
    Export-Csv -Path "$env:TEMP\InactiveTeams.csv" -NoTypeInformation

Write-Host "Found $($inactiveTeams.Count) inactive teams. Exported to InactiveTeams.csv"

The -Period parameter takes D7, D30, D90, or D180 — there’s no arbitrary date range shorter than that, and a specific -Date only works for dates inside the last 28 days. If you’ve seen older scripts that try to determine Teams inactivity by searching the unified audit log for message and file-access events and matching them to a team ID, that approach is fragile and not how Microsoft’s own reporting does it — this report endpoint exists specifically so you don’t have to reconstruct activity from raw audit entries yourself.


Prioritizing by guest exposure, not just silence

A tenant with hundreds of inactive Teams doesn’t need to be worked through in random order — the ones with external guests still sitting in them are the actual priority, not just the oldest or the quietest. Layer a guest check on top of the inactive list, so it only runs against the (much smaller) set of teams that already failed the activity check, rather than auditing every team in the tenant for guests up front:

Connect-MicrosoftTeams

$highPriority = foreach ($team in $inactiveTeams) {
    $guests = Get-TeamUser -GroupId $team.'Team Id' | Where-Object { $_.Role -eq "Guest" }
    if ($guests) {
        [PSCustomObject]@{
            TeamName        = $team.'Team Name'
            LastActivity    = $team.'Last Activity Date'
            GuestCount      = $guests.Count
        }
    }
}

$highPriority | Sort-Object GuestCount -Descending |
    Export-Csv -Path "$env:TEMP\InactiveTeamsWithGuests.csv" -NoTypeInformation

An inactive team with no guests is mostly a housekeeping item — clean it up whenever there’s time. An inactive team that’s still sitting there with external accounts able to access it is a real, live exposure that’s been quietly aging since whatever project it supported wrapped up; that’s the one worth escalating past the standard notify-then-archive grace period the best practices below describe.


When the report comes back anonymized

Before assuming the script above is broken because Team Name shows up as an opaque string instead of an actual name: Microsoft anonymizes usage report data tenant-wide by default, and this applies to every path into the data — admin center reports, the Graph reporting API, and Get-MgReportTeamActivityDetail included. It’s a real privacy setting, not a bug in the cmdlet, and it silently defeats the entire point of this workflow, since the whole goal is knowing which team to act on.

Turning it off: Microsoft 365 admin center > Settings > Org settings > Services > Reports, then uncheck Display concealed user, group, and site names in all reports. It’s also readable and settable programmatically via Graph’s /beta/admin/reportSettings endpoint (the displayConcealedNames property), which is worth wiring into the same script if this report needs to run unattended and can’t rely on someone having flipped the setting in the portal beforehand.


Best practices
  • Don’t delete on the first pass. Notify owners and give a grace period before archiving or removing anything the report flags.
  • Tag Teams with metadata (project, department, creation date) if you’re doing this regularly — it turns a raw inactivity list into something you can actually triage by owner.
  • Run it on a schedule (quarterly is common) rather than as a one-off, since Teams sprawl accumulates continuously.
  • Log what the report flagged and what action was taken — if a “dead” Team turns out to matter to someone, you want a record of when it was flagged and who was notified.

The Teams activity report is a purpose-built Graph endpoint with its own Last Activity Date column — don’t reconstruct it from audit log searches when Microsoft already did the work.

When would you actually use this?
  • A quarterly governance review needs a list of stale Teams to send to owners before anything gets archived.
  • You’re auditing external guest access and want to prioritize Teams that are both inactive and have guests still in them.
  • Licensing or storage costs are climbing and inactive Teams are a suspect worth ruling in or out with real data instead of guessing.


That should get you moving. Let me know in the comments if it doesn’t.


App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning

2 thoughts on “Get All Inactive Microsoft Teams with PowerShell – A Quick Guide”

  1. vorbelutr ioperbir

    As I site possessor I believe the content material here is rattling great , appreciate it for your efforts. You should keep it up forever! Good Luck.

  2. Pingback: Teams Management with PowerShell : What you need to know - Tips by Bits

Leave a Comment

Your email address will not be published. Required fields are marked *