Teams Management with PowerShell : What you need to know

The Microsoft Teams PowerShell module lets you manage teams, channels, members, and policies in bulk — anything that’s tedious clicking through the admin center one team at a time.


In this post: Create a team and add members · Report on all teams · Find inactive teams · Auditing guest access across every team · Archiving vs. actually deleting a team · Bulk policy assignment · PowerShell vs the alternatives · When would you actually use this? · References · Related reading


Create a team and add members
# Connect to Microsoft Teams
Connect-MicrosoftTeams

# Create a new Team
$team = New-Team -DisplayName "Project Alpha Team" -Visibility Private -Description "Team for Project Alpha collaboration"

# Add members
Add-TeamUser -GroupId $team.GroupId -User "jane.doe@yourcompany.com"
Add-TeamUser -GroupId $team.GroupId -User "john.doe@yourcompany.com"
Report on all teams
# Get all Teams
$teams = Get-Team

# Export basic info
$teams | Select DisplayName, Description, Visibility, Archived | Export-Csv "AllTeamsReport.csv" -NoTypeInformation
Find inactive teams

Worth correcting directly: there’s no Get-TeamActivityReport cmdlet in the Teams module — that one shows up in some older guides but doesn’t actually exist. Team-level activity data comes from the Microsoft Graph Reports API instead, via the Microsoft Graph PowerShell SDK, and it works differently: it exports a CSV rather than returning live objects you can query directly.

Connect-MgGraph -Scopes "Reports.Read.All"

# Pull the last-180-days team activity report to a CSV
$reportPath = "$env:TEMP\TeamsActivity.csv"
Get-MgReportTeamActivityDetail -Period 'D180' -OutFile $reportPath

# Import it and find teams with no activity in 6 months
$activityData = Import-Csv -Path $reportPath
$cutoff = (Get-Date).AddMonths(-6)

$activityData | Where-Object {
    [string]::IsNullOrEmpty($_.'Last Activity Date') -or
    [datetime]$_.'Last Activity Date' -lt $cutoff
} | ForEach-Object {
    Write-Host "$($_.'Team Name') is inactive since $($_.'Last Activity Date')"
}

Auditing guest access across every team

Referenced further down as a use case (“find which teams have external members”) but not actually shown — here’s the script. Get-TeamUser returns every team’s members with a Role property, and guest accounts show up there as Role -eq "Guest":

Connect-MicrosoftTeams

$allTeams = Get-Team
$teamsWithGuests = @()

foreach ($t in $allTeams) {
    $guests = Get-TeamUser -GroupId $t.GroupId | Where-Object { $_.Role -eq "Guest" }
    if ($guests) {
        $teamsWithGuests += [PSCustomObject]@{
            TeamName  = $t.DisplayName
            GuestCount = $guests.Count
            Guests    = ($guests.User -join "; ")
        }
    }
}

$teamsWithGuests | Export-Csv "TeamsWithGuests.csv" -NoTypeInformation
Write-Host "$($teamsWithGuests.Count) teams have at least one guest."

This loops every team in the tenant, which is fine for a periodic audit but slow at real scale (hundreds or thousands of teams) since it’s one Get-TeamUser call per team. For a tenant that size, the Graph-based alternative (Get-MgGroupMemberAsUser filtered on UserType -eq "Guest") is generally faster and worth switching to if this audit becomes a recurring, scheduled job rather than an occasional check.


Archiving vs. actually deleting a team

“Archive teams that have gone quiet” shows up as a use case further down, but the inactive-teams script above only finds candidates — it doesn’t act on them, and the two real options here aren’t interchangeable. Set-TeamArchivedState freezes the team (no new posts or file changes) while leaving everything intact and reversible; owners and admins can still manage membership, and the connected SharePoint site can optionally be set read-only at the same time, but doesn’t have to be:

Connect-MicrosoftTeams

foreach ($t in $inactiveTeams) {
    Set-TeamArchivedState -GroupId $t.GroupId -Archived $true -SetSpoSiteReadOnlyForMembers $true
}

# Reversing it later is the same cmdlet with the flag flipped
Set-TeamArchivedState -GroupId $t.GroupId -Archived $false

Remove-Team is a different action entirely, not a stronger version of archiving: it deletes the connected SharePoint site and every file in it, along with the team’s mailbox, calendar, and any connected Planner/OneNote/Power BI content — and it isn’t instant, taking up to an hour to fully complete. There’s a recovery window through the SharePoint admin center afterward, but it’s time-limited, unlike an archived team which stays retrievable indefinitely with one cmdlet. For the “gone quiet for 6 months” scenario this post already describes, archiving is almost always the safer default — reach for Remove-Team only once it’s confirmed nobody needs the content back, not as the first move on an inactive team.


Bulk policy assignment

This is the other place PowerShell earns its keep over the admin center — assigning a policy to a whole department at once instead of one user at a time. Pull the target users from Entra ID by department, then grant the policy in a loop:

Connect-MicrosoftTeams
Connect-MgGraph -Scopes "User.Read.All"

$salesUsers = Get-MgUser -Filter "department eq 'Sales'" -All

foreach ($user in $salesUsers) {
    Grant-CsTeamsMeetingPolicy -PolicyName "SalesExternalMeetings" -Identity $user.UserPrincipalName
}

The same pattern works for messaging policies, calling policies, or app permission policies — swap the cmdlet, keep the same “filter users, loop, grant” structure. This is the actual answer to “assign this policy to 200 people” that doesn’t involve 200 clicks in the admin center.


PowerShell vs the alternatives
FeaturePowerShellTeams Admin CenterGraph APIPower Automate
AutomationFull scriptingManualYes, with codingVisual flows
Bulk operationsEasy via loopsVery limitedPossibleHard at scale
Ease of useModerateEasiestNeeds dev skillsFriendly UI
Best forAdmins, engineersNew admins, light usageDevelopers, custom integrationCitizen developers, light tasks

Full control and scriptability, at the cost of needing to actually know the cmdlets before running them against production teams.

When would you actually use this?
  • Onboarding automation: auto-create a team and channels when HR adds a new department, instead of someone remembering to do it manually.
  • Bulk policy enforcement: assign messaging or meeting policies to a whole group of users by location or department in one script instead of one-by-one.
  • Guest access auditing: find which teams have external members and report on their access — something that’s painful to check per-team in the UI.
  • Lifecycle cleanup: archive teams that have gone quiet for 6+ months, using the exact script above.

The tradeoff versus the admin center UI: full control and scriptability, at the cost of needing to actually know the cmdlets and test scripts before running them against production teams.


References


That’s the rundown. Let me know how it goes for you.


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

Leave a Comment

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