Provisioning Teams by hand is repetitive. Provisioning 50 Teams for 50 new project workgroups by hand is a full day of work. The Microsoft Graph API and PowerShell SDK turn that into a script you run once. This guide is the practical reference: the scopes you need, the cmdlets that actually work in 2026, and the patterns for the four most common operations โ create a team, add channels, manage membership, and post messages.
Free PDF cheat sheet at the bottom condenses everything onto a single page.
Table of Contents
Required scopes
For most Teams admin scripts:
Team.Createโ create new TeamsTeamSettings.ReadWrite.Allโ modify team-level settingsTeamMember.ReadWrite.Allโ manage members and ownersChannel.Create,Channel.Delete.All,ChannelSettings.ReadWrite.Allโ channel opsChannelMessage.Send(delegated) orChannelMessage.ReadWrite.All(application) โ post messagesGroup.ReadWrite.Allโ required because Teams sit on top of M365 groups
Posting messages as the app (not as a user) requires the Resource-Specific Consent (RSC) permission model โ a separate setup we cover at the end.
Team templates
Microsoft ships a few baseline templates: standard, educationClass, educationProfessionalLearningCommunity, plus any custom ones you have published. Most admin scripts use standard.
Create a team
$team = New-MgTeam -BodyParameter @{
"template@odata.bind" = "https://graph.microsoft.com/v1.0/teamsTemplates('standard')"
displayName = "Project Atlas"
description = "Engineering for Project Atlas"
visibility = "Private" # or Public
members = @(
@{
"@odata.type" = "#microsoft.graph.aadUserConversationMember"
roles = @("owner")
"user@odata.bind" = "https://graph.microsoft.com/v1.0/users('alice@contoso.com')"
}
)
}
# $team.Id is the Group Id - the team uses the same Id as its underlying M365 group
Provisioning is asynchronous. The cmdlet returns immediately; the team is fully ready a few seconds later. Poll for readiness if your next step depends on it:
$ready = $false
1..10 | ForEach-Object {
Start-Sleep -Seconds 3
if (Get-MgTeam -TeamId $team.Id -ErrorAction SilentlyContinue) {
$ready = $true; break
}
}
Channels โ public, private, shared
# Standard public channel
New-MgTeamChannel -TeamId $team.Id -BodyParameter @{
displayName = "engineering"
description = "Engineering work"
membershipType = "standard"
}
# Private channel
New-MgTeamChannel -TeamId $team.Id -BodyParameter @{
displayName = "leads-only"
membershipType = "private"
members = @(@{
"@odata.type" = "#microsoft.graph.aadUserConversationMember"
roles = @("owner")
"user@odata.bind" = "https://graph.microsoft.com/v1.0/users('alice@contoso.com')"
})
}
# Shared channel (cross-tenant)
New-MgTeamChannel -TeamId $team.Id -BodyParameter @{
displayName = "vendor-sync"
membershipType = "shared"
}
Add and remove members and owners
$user = Get-MgUser -UserId "carol@contoso.com"
# Add as member
New-MgTeamMember -TeamId $team.Id -BodyParameter @{
"@odata.type" = "#microsoft.graph.aadUserConversationMember"
roles = @()
"user@odata.bind" = "https://graph.microsoft.com/v1.0/users('$($user.Id)')"
}
# Promote to owner
$mem = Get-MgTeamMember -TeamId $team.Id |
Where-Object { $_.AdditionalProperties.email -eq "carol@contoso.com" }
Update-MgTeamMember -TeamId $team.Id -ConversationMemberId $mem.Id -BodyParameter @{
"@odata.type" = "#microsoft.graph.aadUserConversationMember"
roles = @("owner")
}
# Remove
Remove-MgTeamMember -TeamId $team.Id -ConversationMemberId $mem.Id
Post messages and replies
$channel = Get-MgTeamChannel -TeamId $team.Id |
Where-Object DisplayName -eq "general"
# Post a message
$msg = New-MgTeamChannelMessage -TeamId $team.Id -ChannelId $channel.Id -BodyParameter @{
body = @{
contentType = "html"
content = "Welcome to Project Atlas โ kickoff meeting Friday 10:00 CEST."
}
}
# Reply to it
New-MgTeamChannelMessageReply -TeamId $team.Id -ChannelId $channel.Id -ChatMessageId $msg.Id -BodyParameter @{
body = @{ contentType = "text"; content = "Calendar invite sent." }
}
Posting as the application (no signed-in user) is restricted to specific scopes and requires Protected APIs approval from Microsoft for production use. Posting as a user (delegated) works without that step.
Add tabs and apps
# Add a Wiki tab
New-MgTeamChannelTab -TeamId $team.Id -ChannelId $channel.Id -BodyParameter @{
displayName = "Project Wiki"
"teamsApp@odata.bind" = "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/com.microsoft.teamspace.tab.wiki"
}
# Add a website tab
New-MgTeamChannelTab -TeamId $team.Id -ChannelId $channel.Id -BodyParameter @{
displayName = "Roadmap"
"teamsApp@odata.bind" = "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/com.microsoft.teamspace.tab.web"
configuration = @{
contentUrl = "https://confluence.contoso.com/atlas/roadmap"
websiteUrl = "https://confluence.contoso.com/atlas/roadmap"
}
}
Archive / unarchive / delete
# Archive (read-only, keeps content)
Invoke-MgGraphRequest -Method POST -Uri "/v1.0/teams/$($team.Id)/archive"
# Unarchive
Invoke-MgGraphRequest -Method POST -Uri "/v1.0/teams/$($team.Id)/unarchive"
# Delete (hard - same as deleting the underlying M365 group)
Remove-MgGroup -GroupId $team.Id
Cheat sheet
Every cmdlet, scope, and snippet on a single PDF: Teams Graph API Cheat Sheet.
FAQ
Why is my new team missing from the Teams client?
Either the user has not refreshed the Teams client (it polls every few minutes), or the team was created with no members yet. Add at least one member; the team appears for them on next refresh.
Can I post a message as a bot or service account?
Yes โ but posting as the application (no user) requires the Teams "Protected APIs" approval from Microsoft for any production scenario, and even then is rate-limited. Posting as a service account user with delegated scopes is the lower-friction route.
What is the difference between standard, private, and shared channels?
Standard = visible to all team members. Private = visible only to channel members (subset of team). Shared = can include guests from outside the team or even outside the tenant.
How do I find the Team ID without going to the Teams client?
Get-MgTeam -All lists every team. Or Get-MgGroup -Filter "displayName eq 'Project Atlas'" โ the Group ID is the Team ID.
Can I provision a team from a custom template?
Yes โ first publish the template via the Teams admin center or PowerShell, then bind to its template ID instead of standard in the create call.
How do I add a guest user from another tenant?
First invite them to the directory with New-MgInvitation, then add the resulting user object to the team like any other member.
Are Teams operations rate-limited?
Yes โ Teams APIs are particularly aggressive about throttling. Add randomized 1-3 second delays between team-create operations when bulk-provisioning more than ~20 teams.