Introduction
Microsoft Teams environments gradually accumulate obsolete channels over time. These channels, created for temporary needs or completed projects, often remain orphaned without administrative intervention. This situation degrades user experience, pollutes search results, and in the context of Microsoft 365 Copilot, risks contaminating AI-generated responses with obsolete information extracted from archived conversations. This article addresses a systematic strategy for detecting these inactive channels and implementing a recurring management process.
Limitations of native Teams administration tools
Absence of inactivity reports in the admin center
Microsoft does not provide a native report to precisely identify inactive channels in the Teams administration interface. The standard Teams usage report includes an "active channels" column that is of limited usefulness, as it does not reveal which specific channels exhibit prolonged inactivity. Attempts to use Copilot for Microsoft 365 in the context of Teams administrative management return guidance toward "inactive collaboration" features that do not address the real need for granular identification of dormant channels.
Limitation of available metrics
The standard collaborative dashboard in the Teams admin center provides only global aggregates, without per-channel detail. This gap requires a programmatic approach using Microsoft Graph APIs to extract and analyze detailed activity data.
Solution architecture: Microsoft Graph PowerShell SDK
Technical prerequisites and permissions
The implementation requires an app-only session with the Microsoft Graph PowerShell SDK configured with the following permissions:
Team.ReadBasic.All: enumeration of Teams teamsChannel.ReadBasic.All: reading channel metadataChannelMessage.Read.All: access to message historyChannelMember.Read.All: retrieval of channel membership
Important: App-Only mode required
A delegated session will not work, as it would require the authenticated account to be a member of all channels (shared, private, and standard) in the tenant. This situation is practically impossible to achieve in production. You must use an application registered in Microsoft Entra ID with admin consent applied to app-only permissions.
Inactivity detection logic
The detection algorithm works according to the following steps:
- Enumeration of teams via
Get-MgTeam - Iteration over channels of each team with
Get-MgTeamChannel - Extraction of the last message from the channel via
Get-MgTeamChannelMessage - Filtering of system messages: exclusion of automatic notifications (member additions, configuration changes, etc.)
- Temporal comparison: if the last human message is older than a defined threshold (180 days by default), the channel is marked as inactive
Tip: Differentiation of human vs system messages
System messages posted automatically by Teams (membership notifications, configuration changes, etc.) do not reflect actual activity. Filtering based on the messageType property of the message is essential to correctly identify the last genuine user engagement.
Implementation of the detection script
Initialization and configuration
1# Configuration of inactivity parameters2$InactivityThresholdDays = 1803$CutoffDate = (Get-Date).AddDays(-$InactivityThresholdDays)4 5# App-only authentication6Connect-MgGraph -ClientId "<APP_ID>" -TenantId "<TENANT_ID>" -CertificateThumbprint "<THUMBPRINT>"7 8# Variables for report accumulators9$InactiveChannels = @()Enumeration and detection loop
1# Retrieve all teams2$Teams = Get-MgTeam -All3 4foreach ($Team in $Teams) {5 Write-Host "Processing team: $($Team.DisplayName)" -ForegroundColor Cyan6 7 # Enumeration of channels (standard, shared, and private)8 $Channels = Get-MgTeamChannel -TeamId $Team.Id -All9 10 foreach ($Channel in $Channels) {11 # Retrieve channel messages (limited to 1 for performance)12 $Messages = Get-MgTeamChannelMessage -TeamId $Team.Id -ChannelId $Channel.Id `13 -PageSize 1 -All | Where-Object { $_.MessageType -ne 'systemEventMessage' }14 15 if ($null -eq $Messages -or $Messages.Count -eq 0) {16 $LastMessageDate = $null17 $LastMessageAuthor = "No messages"18 }19 else {20 $LastMessage = $Messages[0]21 $LastMessageDate = $LastMessage.CreatedDateTime22 $LastMessageAuthor = $LastMessage.From.User.DisplayName23 }24 25 # Inactivity detection26 if ($null -eq $LastMessageDate -or $LastMessageDate -lt $CutoffDate) {27 $InactiveChannels += [PSCustomObject]@{28 TeamName = $Team.DisplayName29 TeamId = $Team.Id30 ChannelName = $Channel.DisplayName31 ChannelId = $Channel.Id32 LastMessageDate = $LastMessageDate33 LastMessageAuthor = $LastMessageAuthor34 ChannelType = $Channel.DisplayName -match '^General$' ? 'Standard' : 'Shared/Private'35 DaysInactive = if ($null -ne $LastMessageDate) { 36 ((Get-Date) - $LastMessageDate).Days 37 } else { 38 'N/A' 39 }40 TeamOwners = @(Get-MgTeamOwner -TeamId $Team.Id).User.UserPrincipalName -join '; '41 }42 }43 }44}45 46# Export and report47$InactiveChannels | Export-Csv -Path "InactiveChannels.csv" -NoTypeInformation -Encoding UTF848Write-Host "$($InactiveChannels.Count) inactive channels detected" -ForegroundColor YellowGenerating audit reports
Export to Excel with formatting
1# Installation of module for Excel2Install-Module ImportExcel -Force3 4# Creation of an Excel workbook with formatting5$ExcelParams = @{6 Path = "TeamsInactiveChannelsReport.xlsx"7 WorksheetName = "Inactive Channels"8 AutoSize = $true9 TableStyle = "Medium3"10 FreezeTopRow = $true11}12 13$InactiveChannels | Select-Object TeamName, ChannelName, LastMessageDate, `14 DaysInactive, LastMessageAuthor, TeamOwners | `15 Export-Excel @ExcelParams16 17Write-Host "Report generated: TeamsInactiveChannelsReport.xlsx"Distribution of report by email
1# Configuration of report sending2$EmailParams = @{3 To = "teams-admins@contoso.com"4 From = "automation@contoso.com"5 Subject = "Teams Activity Report - Inactive Channels $(Get-Date -Format 'yyyy-MM-dd')"6 Body = @"7Analysis report of inactive Microsoft Teams channels.8 9Number of inactive channels (>180 days): $($InactiveChannels.Count)10Report date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')11 12See attachment for detailed list.13"@14 Attachments = @("TeamsInactiveChannelsReport.xlsx")15 SmtpServer = "smtp.office365.com"16 Port = 58717 UseSsl = $true18 Credential = (Get-Credential)19}20 21Send-MailMessage @EmailParamsManagement of detected inactive channels
Secure archiving of channels
Archiving is the recommended approach as it preserves history and conversations for compliance and audit:
1foreach ($InactiveChannel in $InactiveChannels) {2 try {3 # Archiving of the channel via Microsoft Graph4 Invoke-MgArchiveTeamChannel -TeamId $InactiveChannel.TeamId `5 -ChannelId $InactiveChannel.ChannelId6 7 Write-Host "✓ Archived: $($InactiveChannel.ChannelName) in $($InactiveChannel.TeamName)" `8 -ForegroundColor Green9 }10 catch {11 Write-Error "✗ Error archiving $($InactiveChannel.ChannelName): $_"12 }13}Deletion of channels (destructive approach)
Deletion is irreversible and should only be used for test or temporary channels:
1# Selective deletion with confirmation2$ChannelsToDelete = $InactiveChannels | Where-Object { $_.DaysInactive -gt 365 }3 4foreach ($Channel in $ChannelsToDelete) {5 $Confirmation = Read-Host "Delete channel '$($Channel.ChannelName)'? (Y/N)"6 7 if ($Confirmation -eq 'Y') {8 Remove-MgTeamChannel -TeamId $Channel.TeamId -ChannelId $Channel.ChannelId9 Write-Host "✓ Deleted: $($Channel.ChannelName)" -ForegroundColor Green10 }11}Warning: Irreversible deletion
The Remove-MgTeamChannel cmdlet permanently deletes the channel and all its conversations. No recycle bin or recovery point exists for Teams. Perform a backup of the content via export before deletion.
Recurring automation with Azure Automation
Runbook configuration
To execute this detection on a regular basis (weekly or monthly), implement an Azure Automation runbook:
Create the Automation account
In the Azure portal, create an Azure Automation account with a managed identity having appropriate Graph permissions consented via Entra ID.
Import the Graph module
In the Automation account, import the Microsoft.Graph.Teams module from the Azure modules catalog.
Create the PowerShell runbook
Create a runbook of type PowerShell Workflow or Python 3.8+ containing the detection script. Configure authentication with the managed identity:
1$connectionName = "AzureRunAsConnection"2try {3 $servicePrincipalConnection = Get-AutomationConnection -Name $connectionName4 5 Connect-MgGraph -ClientId $servicePrincipalConnection.ApplicationId `6 -TenantId $servicePrincipalConnection.TenantId `7 -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint8}9catch {10 throw "Unable to connect to Azure Automation: $_"11}Configure the schedule
Create a schedule (Schedule) to run the runbook every Monday at 02:00 UTC and associate it with the runbook via a schedule link.
Configure notifications
Add an Azure Monitor Action Group action to send reports by email after each execution, using the runbook outputs.
Managed configuration variables
1# Define these variables in the Automation account for flexibility2$InactivityThreshold = Get-AutomationVariable -Name "TeamsInactivityDays"3$ReportRecipient = Get-AutomationVariable -Name "ReportEmailAddress"4$StorageAccountName = Get-AutomationVariable -Name "AuditStorageAccount"Considerations on indexing and Copilot
Impact on search and generative AI
Conversations in inactive channels continue to be indexed by Microsoft 365 search services. When Microsoft 365 Copilot generates responses based on organizational context:
- Obsolete data from old channels may be included in results
- Outdated team/project statuses introduce imprecision
- Project-related context conversations create noise
Regular archiving of inactive channels mitigates this data contamination, ensuring that Copilot relies on relevant and current information.
Good to know
Archiving does not remove existing indexing. Conversations remain searchable but are marked as archived. For complete isolation of obsolete data from Copilot, deletion remains the only recourse, with associated risks.
Recommended governance strategy
Validation process before action
- Monthly execution of the detection script
- Manual review of the report by team owners (notification via email)
- 15-day claim period: team owners can request channel retention
- Automated archiving of unclaimed channels via the runbook
- Retention of audit logs of all actions for compliance
Audit controls in the Microsoft 365 activity log
1# Search for Teams channel archiving in audit logs2Search-UnifiedAuditLog -RecordType TeamsAdmin -Operations ArchiveTeamChannel `3 -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) |4 Select-Object UserIds, CreationDate, ObjectId |5 Export-Csv -Path "TeamsArchiveAudit.csv"Troubleshooting common issues
Error: "Insufficient privileges to complete the operation"
Verify that:
- The Entra ID application has admin consent applied
- Graph permissions are consented at the tenant level (not just user)
- The managed identity of Azure Automation has the necessary roles
Performance: Script slow on large tenants
To optimize on 10,000+ teams:
1# Use pagination and parallelization2$Teams = Get-MgTeam -All -PageSize 9993 4# Parallel processing with runspace pools5$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 4)6$RunspacePool.Open()"System" messages are not correctly filtered
Verify the exact schema of the message type:
1# Diagnostic: examine all message types2$Messages = Get-MgTeamChannelMessage -TeamId $TeamId -ChannelId $ChannelId -All3$Messages | Group-Object -Property MessageType | Select-Object Name, CountConclusion and next steps
The systematic identification of inactive channels in Microsoft Teams is an essential element of data governance. By implementing an automated process via Microsoft Graph PowerShell SDK and Azure Automation, you maintain a clean and performant collaborative environment while preserving data integrity for compliance and Copilot.
The complete script is available on the GitHub Office365itpros for download and reuse. Adapt the inactivity thresholds, permissions, and notification workflows according to your specific organizational policies.



