Choosing Between Modules
The answer isn’t straightforward because it depends on what you’re trying to do. Here are some points to remember:
- Both modules have links to the Microsoft Graph. Many of the cmdlets in the Microsoft Teams module are based on Graph APIs and all the cmdlets in the Microsoft Graph PowerShell SDK are derived from Graph APIs.
- The Microsoft Teams module includes other cmdlets that are not based on Graph APIs. Most are policy management cmdlets that originally came from the Skype for Business connector.
- The Microsoft Graph PowerShell SDK includes cmdlets to interact with other Microsoft 365 workloads and Entra ID.
- The Microsoft Teams module is designed to automate management operations. When used by an account holding the global administrator or Teams administrator role, the cmdlets can manage all teams in an organization.
- The Microsoft Graph PowerShell SDK includes cmdlets that can interact with user content, such as posting new chats. Different permissions are needed to interact with different Teams data. Developers also need to remember the difference between delegated permissions and application permissions when it comes to accessing data.
The remainder of this article covers the basics of Teams messaging and explains how to post messages to one-to-one and group chats and channel conversations using the Microsoft Graph PowerShell SDK. The process of removing messages from channels is covered in a different article.
A Note on Permissions
As you probably know, the permissions model used for Graph API requests is a least privilege basis. In most cases, application permissions are available to allow apps that use Graph requests to run as background processes such as a scheduled Azure Automation job.
No application permissions are available for Graph requests like posting messages to chats or channel conversations, so delegated permissions must be used. The app needs the ChannelMessage.Send permission to post messages to channels and the Chat.Create permission to post to a chat. Further permissions (like Chat.ReadWrite) are required to fetch messages.
Because of the lack of application permissions, the signed-in account must be a member of a chat or team to post messages and a team owner to remove channel messages. Azure Automation managed identities cannot be used because these identities cannot be members of a team or chat.
The usual workaround is to sign into Teams using a utility account that’s a team member and post to channels that way. In the past I’ve done this using the Submit-PnPTeamsChannelMessage cmdlet PnP module using credentials stored in Azure Key Vault. The same technique works with the cmdlets covered here.
Chat Payloads
Teams supports one-on-one and group chats. The first example creates a new one-on-one chat between two tenant accounts. The Microsoft documentation includes several good examples of how to post messages to chats. However, I think the method used to construct the JSON payload to post to Teams is overly complicated and doesn’t lend itself to programmatic manipulation.
I therefore take a different approach to define and pop the arrays and hash tables that form the payload. It’s the same approach as used to build parameters to post Planner tasks to a plan. Use the approach taken in the documentation examples if you like. They work and are good examples, but I think my method is better for production scripts.
Creating and Posting to a One-to-One Chat
First, we define an array holding the account identifiers for the chat participants and the hash table holding the payload (aka the request body) used to create the chat. The code also adds a parameter to identify the chat as one-to-one. You must use the exact format (onOnOne) to identify the chat type.
[array]$MemberstoAdd = "eff4cd58-1bb8-4899-94de-795f656b4a18", "cad05ccf-a359-4ac7-89e0-1e33bf37579e" $OneOnOneChatBody = @{} $OneOnOneChatBody.Add("chattype", "oneOnOne")
$Members = @() ForEach ($Member in $MemberstoAdd) { $User = Get-MgUser -UserId $Member -Property id, displayName, userType, userprincipalName $MemberId = ("https://graph.microsoft.com/v1.0/users('{0}')" -f $Member) $MemberDetails = @{} [array]$MemberRole = "owner" If ($User.userType -eq "Guest") { [array]$MemberRole = "guest" } $MemberDetails.Add("roles", $MemberRole.trim()) $MemberDetails.Add("@odata.type", "#microsoft.graph.aadUserConversationMember") $MemberDetails.Add("user@odata.bind", $MemberId.trim()) $Members += $MemberDetails } $OneOnOneChatBody.Add("members", $Members)
The complete payload looks like this:
$OneOnOneChatBody Name Value ---- ----- chattype oneOnOne members {System.Collections.Hashtable, System.Collections.Hashtable}
$NewChat = New-MgChat -BodyParameter $OneOnOneChatBody If ($NewChat) { Write-Host ("New chat {0} created" -f $NewChat.id) } Else { Write-Host "Failed to create chat" }
$Content = "<b><i>Using the Microsoft AuditLog Query Graph API (Preview)</b></i><p>" $Content = $Content + "The unified audit log is the source of a lot of information about a Microsoft 365 tenant. The Search-UnifiedAuditLog cmdlet is available to search the audit log and now we have the AuditLog Graph API. This article explains how to use the new API to query and retrieve audit records from the log." $Content = $Content + "<p>For more information, see <a href='https://practical365.com/audit-log-query-api/'>https://practical365.com/audit-log-query-api/</a>" $ChatMessageBody = @{} $ChatMessageBody.Add("contentType", "html") $ChatMessageBody.Add("content", $Content) $ChatMessage = New-MgChatMessage -ChatId $NewChat.Id -Body $ChatMessageBody
Figure 1 shows the message posted to the chat.

Creating and Posting to a Group Chat
$Members = @() $Team = Get-MgTeam -Filter "displayName eq 'Information Quality and Accuracy'" If ($Team) { [array]$TeamMembers = (Get-MgGroupMember -GroupId $Team.Id).Id } # Create a new group chat $GroupChatBody = @{} $GroupChatBody.Add("chattype", "group") $GroupChatBody.Add("topic", "Major Updates - Urgent News")
ForEach ($Member in $TeamMembers) { $User = Get-MgUser -UserId $Member -Property id, displayName, userType, userprincipalName # Exclude MTO accounts If (($User.userprincipalName -notlike "*#EXT#*" -and $User.userType -eq "Member") -or $User.userType -eq "Guest") { Write-Host ("Processing member {0}" -f $User.DisplayName) $MemberId = ("https://graph.microsoft.com/v1.0/users('{0}')" -f $Member) $MemberDetails = @{} [array]$MemberRole = "owner" If ($User.userType -eq "Guest") { [array]$MemberRole = "guest" } $MemberDetails.Add("roles", $MemberRole.trim()) $MemberDetails.Add("@odata.type", "#microsoft.graph.aadUserConversationMember") $MemberDetails.Add("user@odata.bind", $MemberId.trim()) $Members += $MemberDetails } } # Add the membership information to the parameter body $GroupChatBody.Add("members", $Members) $NewGroupChat = New-MgChat -BodyParameter $GroupChatBody
Figure 2 shows the membership of the group chat created by the code above.

The same New-MgChatMessage command used to post a message to a one-to-one chat will post a message to the group chat.
Multiple Group Chats
$GroupChats = Get-MgChat -filter "ChatType eq 'group'" $GroupChats | Format-Table Topic, Id Topic Id ----- -- Major Updates - Urgent News 19:1741422a23464620a6e71f363db2d51a@thread.v2 Major Updates - Urgent News 19:949d2f6c2b1f4f50b52714ddb2be4a36@thread.v2 Major Updates - Urgent News 19:5f6deae375f84b3396a9648722ba62b8@thread.v2 Warnings 19:b2db6ce5bfb348709dccd9b8f959dfae@thread.v2
To distinguish between the chats with the same topic, you must check other details such as the chat members (Get-MgChatMembers) or the timestamp for the last update.
Posting Base Notes and Replies to a Channel
Teams organizes the messages that compose channel conversations (threads) into base notes and replies. This code finds the identifier for the General channel of the team we used previously and uses the New-MgTeamChannelMessage cmdlet to post a message as a new base note (conversation).
[array]$TeamChannel = Get-MgTeamChannel -TeamId $Team.Id -Filter "displayName eq 'General'" $ChatMessageBody = @{} $ChatMessageBody.Add("contentType", "html") $ChatMessageBody.Add("content", $Content) $ChannelMessage = New-MgTeamChannelMessage -TeamId $Team.Id -ChannelId $TeamChannel.Id -Body $ChatMessageBody -Subject "Really Important News" -Importance "urgent" $MessageContents = Get-MgTeamChannelMessage -TeamId $Team.Id -ChannelId $TeamChannel.Id -ChatMessageId $ChannelMessage.Id If ($MessageContents) { Write-Host ("Channel message posted by {0} at {1}" -f $MessageContents.from.user.displayName, $MessageContents.createdDateTime) }
To add a reply to the conversation, run New-MgTeamChannelMessageReply cmdlet and pass the identifier of the base note.
$ReplyMessageBody = @{} $ReplyMessageBody.Add("contentType", "html") $ReplyMessageBody.Add("content", "This is a reply...") $ReplyMessage = New-MgTeamChannelMessageReply -TeamId $Team.Id -ChannelId $TeamChannel.Id -ChatMessageId $ChannelMessage.Id -Body $ReplyMessageBody If ($ReplyMessage) { Write-Host ("Reply posted by {0} at {1}" -f $ReplyMessage.from.user.displayName, $ReplyMessage.createdDateTime) }
Figure 3 shows the result of posting the base note and a reply to the General channel.

More Teams Messaging Options to Cover
Hopefully, these examples provide some insight into how the Teams messaging cmdlets in the Microsoft Graph PowerShell SDK work. Other possibilities exist, such as including attachments or mentions in messages. The Microsoft documentation will help you explore the necessary parameters or maybe I’ll get to them in a future article.
Table of Contents
Scripting Teams Urgent Messages for a Set of Users
A reader asked if it was possible to write a PowerShell script to send chats to a set of people when something important happened, like a failure in an important piece of plant or a medical emergency. They explained that they have the facility to broadcast this kind of information via email, but a lot of their internal communications have moved to Teams and they’d like to move this kind of scripted communication too.
Teams supports urgent messages for one-to-one chats. Originally, these messages were called priority notifications and Microsoft planned to charge for their use. That idea disappeared in the mists of the Covid pandemic, and anyone can send urgent messages today. The nice thing about urgent messages is that Teams pings the recipient every two minutes until they read the message or twenty minutes elapses.
- Run the Connect-MgGraph cmdlet to connect to the Graph. Delegated permissions must be used, and I specified the Chat.ReadWrite and User.Read.All permissions.
- Because the script works with delegated permissions, the chats are sent by the signed-in user. The script runs the Get-MgContext cmdlet to find out what that account is.
- The script sends chats to a set of users. Any Entra ID group will do.
- The New-MgChatMessage cmdlet eventually sends the chat message. Because I want to include an inline image and a mention in the message, work must be done to construct the payload needed to tell Teams what content to post.
- In Graph requests, this information is transmitted in JSON format. PowerShell cmdlets don’t accept parameters in the same way. Three different parameters are involved – the body, the mention, and the hosted content (image uploaded to Teams). Each parameter is passed as a hash table or array, and if the parameter takes an array, it’s likely to include some hash tables. Internally, Teams converts these structures to JSON and submits them to the Graph request. You don’t need to care about that, but constructing the various arrays and hash tables takes some trial and error to get right. The examples included in Microsoft documentation are helpful but are static examples of JSON that are hard to work with programmatically. I use a different approach. Here’s an example of creating the hash table to hold details of the inline image:
# Create a hash table to hold the image content that's used with the HostedContents parameter $ContentDataDetails = @{} $ContentDataDetails.Add("@microsoft.graph.temporaryId", "1") $ContentDataDetails.Add("contentBytes", [System.IO.File]::ReadAllBytes("$ContentFile")) $ContentDataDetails.Add("contentType", "image/jpeg") [array]$ContentData = $ContentDataDetails
- After populating the hash tables and arrays, the script runs the New-MgChat cmdlet. If an existing one-on-one chat exists for the two users, Teams returns the identifier of that chat thread. If not, Teams creates a new chat thread and returns that identifier.
- The script runs the New-MgChatMessage cmdlet to post the prepared message to the target chat thread. Setting the importance parameter to “urgent” marks this as a Teams urgent message.
$ChatMessage = New-MgChatMessage -ChatId $NewChat.Id -Body $Body -Mentions $MentionIds -HostedContents $ContentData -Importance Urgent
The Urgent Teams Message

You can download the script from GitHub.
Plain Sailing After Understanding Parameter Formatting
There’s no doubt that it’s more complicated to create and send one-to-one chats than it is to send email to a group of recipients, especially if you stray away from a very simple message body. However, much of the complexity is getting your head around the formatting of parameter input. Once you understand that, it’s reasonably easy to master the rest of the code.
Learn about using the Microsoft Graph PowerShell SDK and the rest of Microsoft 365 by subscribing to the Office 365 for IT Pros eBook. Use our experience to understand what’s important and how best to protect your tenant.