๐ฌ Recipe ยท Microsoft Teams
Generate a Microsoft Teams Governance Report (Inactive, Ownerless, Guest-heavy)
Audit every Team in your tenant for ownership, guest exposure, and engagement โ then flag candidates for archival, re-owning, or review.
Complexity
Intermediate
Impact
governance + security + compliance + reporting + teams + lifecycle
Context
Why This Matters
Microsoft Teams sprawl is one of the most common governance headaches in M365 tenants. Over time, Teams accumulate: owners leave the company (creating orphaned groups), external guests get invited and never removed, and project Teams outlive their purpose and sit dormant with sensitive content.
A regular Teams governance report gives IT admins a clear, data-driven view of which Teams need attention. This recipe walks you through producing that report โ enumerating every Team in the tenant, checking owner count, counting guests, and flagging Teams that match common risk patterns (orphaned, guest-heavy, low-engagement).
When to run this
- Quarterly governance review cycles
- Before a tenant migration or rename
- As part of an external-sharing audit or compliance review
- After a wave of departures (layoffs, reorgs) to catch newly orphaned Teams
Expected Outcomes
After completing this recipe you will have:
- A CSV or structured report listing every Team in the tenant with Owner count, Member count, Guest count, and a computed governance Status (Healthy / Orphaned / High Guest Count / Low Engagement).
- A Suggested Action column for each Team (e.g. Assign new owner, Review guest access, Consider archiving).
- Aggregate stats: total Teams, number orphaned, number guest-heavy, number with low engagement.
- A repeatable script you can schedule (e.g. monthly) to track governance trends over time.
Risks & Considerations
Warnings & gotchas
- This is read-only. The recipe does not archive, delete, or modify any Team. Treat the output as a decision-support tool, not an automated cleanup.
- Don't auto-archive on one signal. A Team with <3 members could be a legitimate small working group. Always confirm with the owner before archival.
- Guest thresholds are org-specific. The default "high guest count" threshold of 10 is a starting point โ tune it to your org's risk appetite.
- Inactivity needs the Reports API. Truly accurate "inactive" detection (last message posted, last channel activity) requires the
Reports.Read.Allscope and thegetTeamsActivityDetailendpoint. The basic recipe uses member count + created date as a proxy; the richer version is noted in the API section. - Privacy: Teams activity reports can contain usernames. In some jurisdictions you must anonymize before distribution โ check with your DPO.
- Throttling: Tenants with thousands of Teams will hit Graph throttling. Use paging (
$top=100with@odata.nextLink) and exponential backoff.
Required Permissions
| Permission | Why It's Needed |
|---|---|
| Group.Read.All | Enumerate all M365 groups (including Teams), list owners, and list members. |
| Team.ReadBasic.All | Confirm a group is Teams-enabled and read basic Team properties. |
| Reports.Read.All | Optional โ required only if you want true activity data (last message, channel activity) via the Teams usage reports API. |
| User.Read.All | Resolve member userType to accurately count guests vs. internal users. |
The fastest way to get this done: just ask Dex. Copy the prompt below and paste it into your Dex conversation.
For IT Admins
Paste into Dex Pro
Manual walkthrough via Microsoft Teams admin center
The Teams admin center exposes most of this data, but you'll have to assemble the governance view manually across several blades.
Step 1: List all Teams
- Sign in to the Teams admin center as a Global Admin or Teams Administrator.
- Navigate to .
- Use the column chooser (gear icon, top-right of the table) to enable: Owners, Members, Guests, Status, Privacy.
- Click Export to CSV (top of the table) to download the full list.
Step 2: Identify orphaned Teams
- Open the exported CSV in Excel.
- Filter the Owners column for value
0. These are your orphaned Teams โ top priority to re-own.
Step 3: Identify guest-heavy Teams
- Sort or filter the Guests column descending.
- Flag any Team above your chosen threshold (commonly 10 guests or >25% of membership).
Step 4: Identify inactive Teams
- In the Microsoft 365 admin center, go to .
- Set the time range to Last 90 days.
- Export the report and join it (by Team ID) with your Step 1 export to see which Teams had zero channel messages or meetings in 90 days.
Step 5: Document decisions
For each flagged Team, record the recommended action (Archive / Re-own / Review guests / Keep) in a tracking sheet and assign an owner-of-record for follow-up.
Estimated manual time for a tenant with ~200 Teams: 25โ45 minutes.
Graph API recipe
The report is built from three Graph endpoints, called per Team.
1. List all Teams (groups with Team provisioning)
GET https://graph.microsoft.com/v1.0/groups?$filter=resourceProvisioningOptions/any(x:x eq 'Team')&$select=id,displayName,createdDateTime,visibility&$top=100Page with @odata.nextLink until exhausted.
2. Get owners for each Team
GET https://graph.microsoft.com/v1.0/groups/{group-id}/owners?$select=id,displayName,userPrincipalNameOrphaned = value.length === 0.
3. Get members (and count guests)
GET https://graph.microsoft.com/v1.0/groups/{group-id}/members?$select=id,userPrincipalName,userTypeGuests are entries with userType === 'Guest' or UPNs containing #EXT#.
4. (Optional) Add real activity data
GET https://graph.microsoft.com/v1.0/reports/getTeamsTeamActivityDetail(period='D90')Returns per-Team last-activity timestamps, channel message counts, and meeting counts. Requires Reports.Read.All. Join the result on Team ID to classify Teams as Inactive (0 messages in 90 days).
Sample response from endpoint 1
{
"value": [
{
"id": "02bd9fd6-8f93-4758-87c3-1fb73740a315",
"displayName": "Mark 8 Project Team",
"createdDateTime": "2023-04-11T14:22:17Z",
"visibility": "Private"
}
]
}Classification logic
OwnerCount === 0โ Orphaned โ Assign new ownerGuestCount > 10(tune per org) โ High Guest Count โ Review guest accessMemberCount < 3โ Low Engagement โ Consider archivingLastActivityDate < today - 90d(if using Reports API) โ Inactive โ Archive candidate
PowerShell walkthrough
We use the Microsoft Graph PowerShell SDK. The script connects, pulls all Teams, iterates to gather owner/member/guest counts, classifies each Team, and exports a CSV.
Prerequisites
- PowerShell 7+ recommended.
- Install the SDK if needed:
Install-Module Microsoft.Graph -Scope CurrentUser. - Connect with the required scopes:
Connect-MgGraph -Scopes "Group.Read.All", "User.Read.All", "Reports.Read.All"Key logic
Get-MgGroup -Filter "resourceProvisioningOptions/Any(x:x eq 'Team')" -Allpulls every Team-enabled group.- For each group,
Get-MgGroupOwnerandGet-MgGroupMemberbuild the counts. - Members with
UserType -eq 'Guest'increment the guest counter. - A
switchblock assigns Status and Suggestion. - Results are exported with
Export-Csv.
Tuning
Adjust the $GuestThreshold and $MinMembers parameters at the top of the script to match your governance policy.
Scheduling
Wrap the script in a Scheduled Task or Azure Automation runbook using a certificate-based app registration (not interactive Connect-MgGraph) for unattended runs.
Next steps