Power Automate: Teams - When a new chat message is added Trigger

Power Automate: Teams - When a new chat message is added Trigger

by: Manuel 14 min read 0 comments

So you don't have enough anxiety in your life that you need to know when a new chat message arrives in Teams. The "When a new chat message is added" trigger can help with that because it fires whenever a message is posted in any Microsoft Teams chat that the connected user takes part in.

It can be used for multiple things, like notifications for when specific words show up, automatic triage of items, and more, but you should be careful using it. If the Teams chats are quite busy, then this will trigger a lot, meaning that you can exhaust your usage quite quickly.

For those who like technical details, it is a webhook trigger, so it reacts in near real time instead of polling on a schedule, and it needs no configuration at all. You drop it on the Power Automate canvas and it starts listening.

Being a webhook makes it quite fast.

I will go into detail later, but the trigger can be confusing, because the payload it delivers contains no message text, no sender, and no timestamp. It gives you three identifiers and expects you to go and fetch the rest yourself.

Let's see how it works.

Where to find it?

The easiest way is to search for it as follows:

Here's what it looks like.

Four triggers, very similar names

The Microsoft Teams connector ships several message triggers whose names differ by only a word or two, and the screenshot above shows most of them sitting together in the same panel. To make it worse, the picker truncates the longer names, so "When a new message is added to a..." and "When a new channel message is ad..." give you nothing to tell them apart by. Picking the wrong one costs you an afternoon, so please check the table below before you commit.

Trigger Scope Can you narrow it?
When a new chat message is added Every chat the connected user is in No, it has no parameters
When a new message is added to a chat or channel One specific chat or channel you choose Yes, you pick the message type and the target
When a new channel message is added One channel inside one team Yes, you pick the team and the channel
When keywords are mentioned A chat or channel, filtered by a single word Yes, by target and keyword

If you already know which conversation you care about, the second row is almost always the better choice. The trigger described in this article is the blunt instrument of the group, and it earns its place only when you genuinely want everything.

Now that we know how to find it let's understand how to use it.

Usage

There is nothing to configure

Nothing to configure, but you need to know that the connection is important.

The trigger listens on behalf of the account behind that connection, so if Joana sets up the flow with her account, the flow sees Joana's chats and nobody else's.

Swapping the connection later changes which mailbox of conversations the flow watches, so be careful with the connection you set, especially if you use a service account to run the flow.

Trigger conditions, and what they can actually see

Since the trigger has no parameters, a trigger condition is the only way to stop a run before it starts. Trigger conditions live under the Settings tab of the trigger card, right beside the empty Parameters tab, and they are worth using here, because a busy person can easily generate a few hundred runs a day.

A trigger condition can only test values that exist in the trigger output, and the trigger output holds three identifiers and nothing else. You cannot filter by sender, by keyword, by importance, or by message type at the trigger, because none of those values have arrived yet.

What you can do is filter by conversation. If you only care about one chat, this expression keeps every other conversation out of your run history.

@equals(first(triggerBody()?['value'])?['conversationId'], '<insertConversationId>')

Breaking that down:

  • triggerBody() gets the body of the trigger output
  • ?['value'] safely reaches the array of messages the trigger delivered
  • first(...) takes the first entry of that array, using the "first" function
  • ?['conversationId'] safely reads the chat identifier from that entry
  • equals(..., '<insertConversationId>') compares it to the chat you care about, using the "equals" function

Please note that the trigger can deliver more than one message in a single batch, so first only inspects the leading entry. It is good enough for the common case where the batch holds one message, but if you need to be exact, use the "contains" function against the serialized payload instead.

@contains(string(triggerBody()?['value']), '<insertConversationId>')

To find the conversation ID in the first place, run the flow once, open the run history, and copy the value from the trigger output. It looks like 19:a1b2c3d4e5f6@thread.v2 for a group chat.

Getting the message content

Because the trigger gives you identifiers rather than content, almost every real flow starts with a "Get message details" action. That pairing is covered in detail in the next section, since it is really a consequence of the trigger's output shape.

Outputs

The trigger returns a ChatMessageWebhookResponseSchema object. Here is the whole thing, and it is genuinely this small.

Field Type Description Example value
value array of object Message details response An array with one entry in most runs
value.conversationId string The chat's unique identifier 19:a1b2c3d4e5f6@thread.v2
value.messageId string Message ID 1755012345678
value.linkToMessage string Message link https://teams.microsoft.com/l/message/19:a1b2c3d4e5f6@thread.v2/1755012345678

Three identifiers and a deep link. No text, no sender, no timestamp, no importance.

The linkToMessage value is more useful than it first appears. If your flow only needs to notify somebody that a message arrived, you can post that link and let the reader click through to Teams. That avoids a second API call entirely, and it sidesteps every formatting problem that comes with reproducing a message somewhere else.

Everything arrives inside an array

value is an array of object, so the designer wraps the first action you add in an "Apply to each" loop the moment you reference a field inside it. This catches people off guard, especially since the array usually carries exactly one message. Do not fight it, because the batch really can hold more than one entry.

Inside the loop, reach the fields like this.

items('Apply_to_each')?['messageId']
items('Apply_to_each')?['conversationId']
items('Apply_to_each')?['linkToMessage']

Fetching the real message

To get the content, add a "Get message details" action inside that loop. It takes three inputs.

  • Message, which is the messageId from the loop
  • Message type, where you choose Group chat, Meeting chat, or Chat depending on the conversation
  • Conversation, where you pass the conversationId from the loop

The action returns a full Microsoft Teams ChatMessage object. These are the fields you will reach for most often.

Field Type Description
body.content string The content of the message
body.contentType string The type of the content. Possible values are text and html
from.user.displayName string Display name of the sender
from.user.id string The sender's identifier
createdDateTime date-time Timestamp of when the chat message was created
lastModifiedDateTime string Timestamp when the message was created or modified, including when a reaction is added or removed
importance string The importance of the message. The possible values are normal, high, urgent
messageType string The type of chat message
subject string The subject of the chat message, optional
summary string Summary text of the message, useful for push notifications and fallback views
mentions array of object List of entities mentioned in the message. Supported entities are user, bot, team, and channel
attachments array of object Attachments carried by the message
reactions array of object Reactions for this message, for example Like
replyToId string ID of the parent message of the thread
deleted boolean Whether the message was deleted
locale string Locale of the message set by the client

The outputs of "Get message details" are dynamic, so please use safe navigation when you read them.

outputs('Get_message_details')?['body']?['body']?['content']
outputs('Get_message_details')?['body']?['from']?['user']?['displayName']

The doubled ['body'] is not a typo. The first one is the action's response body, and the second is the message's body object.

The body.content value usually arrives as HTML, even for a message that looked like plain text in the client. If you plan to store it or email it, run it through the "HTML to Text" action first.

Non-intuitive behaviors

Here are the behaviors that catch people off guard.

The payload has no message content

This is the big one, and it is worth stating plainly because it undermines the obvious mental model. The trigger is named after chat messages, so people reasonably expect a chat message. What arrives is a receipt telling you that a message exists somewhere, and you have to go and collect it. Every flow built on this trigger costs at least two connector calls per message, one for the trigger and one for "Get message details".

You cannot filter on anything meaningful at the trigger

Because the payload holds only identifiers, a trigger condition can test the conversation and nothing more. Filtering by sender, by keyword, or by importance has to happen inside the flow, after you have fetched the message. That means the run is already consuming your quota by the time you decide you did not want it. Consider that when estimating how many runs a chatty colleague will generate.

It covers chats, never channels

"Any chat the user is a part of" means one to one chats, group chats, and meeting chats. Channel conversations are a separate world in the Microsoft Teams data model, and this trigger never sees them. If you want channel activity, use the When a new channel message is added trigger instead.

The flow's own messages come back around

If your flow posts a message into a chat the connected user belongs to, that post is itself a new chat message in a chat the user is part of, so the trigger fires again. The loop is easy to build by accident and unpleasant to watch. Since the trigger payload has no sender, you cannot break the cycle with a trigger condition, so the guard has to sit inside the flow, after "Get message details", comparing the sender against the account your flow posts as.

It can go quiet without failing

Community reports describe flows that run happily for an hour or two and then stop firing, with no failed runs and no error to investigate. The flow simply sits there looking healthy while messages go unnoticed, which is the worst kind of failure because nothing alerts you.

Documented but not confirmed

The reports point at the underlying webhook subscription expiring, based on a subscriptionExpirationDateTime value that appears in real trigger outputs roughly an hour ahead of the flow starting. That field is not part of the documented schema, and Microsoft has not published an acknowledgement or a fix, so please treat this as a community observation rather than settled behavior. If you hit it, the workarounds people report are switching to the "When keywords are mentioned" trigger, or running a scheduled flow that turns the affected flow off and on again on a timer.

Limitations

Here are the constraints to keep in mind.

One user per flow

Microsoft documents this explicitly. When a new chat message is posted in any chat where you are a participant, the trigger supports only one user per flow. There is no way to watch a second person's chats from the same flow, and no admin setting changes that. Monitoring several people means several flows, several connections, and a licensing conversation.

No parameters, so no scoping

The trigger cannot be narrowed at the source. Every message in every chat reaches your flow, and any filtering you apply is client side and after the fact. For someone in a handful of quiet chats this is fine. For someone living in twenty group chats it produces a run history that is difficult to read and a quota that drains quickly.

Content requires a second call

There is no toggle that makes the trigger return the message body. The "Get message details" round trip is mandatory whenever you need content, which doubles your call count and adds a point of failure.

Connector throttling

The Microsoft Teams connector allows 100 API calls per connection every 60 seconds. Since each message costs you at least one extra call for the details lookup, and often more if you also resolve members or attachments, a busy period can hit that ceiling. Non-GET requests are capped separately at 300 per connection every 300 seconds, dropping to 25 for Flow bot operations.

Private channels and posting limits

If your flow replies as well as listens, remember that posting a message or adaptive card to a private channel is not supported, and a single message can @mention at most 20 users and 20 tags.

Recommendations

Here are some things to keep in mind.

Reach for the scoped sibling first

If you know which conversation you care about, use "When a new message is added to a chat or channel" instead. It takes a message type and a target, so the filtering happens before the run rather than after, and it covers channels as well. Most flows that start with this article's trigger end up wanting that one.

Post the link instead of the message

When the goal is simply to tell somebody that something happened, send linkToMessage rather than rebuilding the message elsewhere. You skip the details lookup, you halve your API usage, and the reader lands in the real conversation with all its context intact instead of reading a stripped copy.

Guard against loops deliberately

If the flow posts anything back into Microsoft Teams, add a Condition right after "Get message details" that stops the run when the sender is the account your flow posts as.

@not(equals(outputs('Get_message_details')?['body']?['from']?['user']?['id'], '<insertUserId>'))
  • outputs('Get_message_details') gets the action's full output
  • ?['body']?['from']?['user']?['id'] safely walks down to the sender's identifier
  • equals(..., '<insertUserId>') checks whether the sender is your flow's account
  • not(...) inverts it, so the flow continues only for messages somebody else wrote, using the "not" function

Filter case insensitively when you look for words

Keyword checks inside the flow should not care whether Beatriz typed "Deploy" or "deploy". Wrap the content with the "toLower" function before comparing.

@contains(toLower(outputs('Get_message_details')?['body']?['body']?['content']), 'deploy')

Please remember that the content is HTML, so a word can be split by markup in ways you did not expect. Converting to text first makes the check far more reliable.

Convert the HTML before you store it

Raw HTML in a SharePoint column or an email body rarely renders the way you hope. Run body.content through the "HTML to Text" action and store the clean version, keeping the original only if you truly need the formatting.

Use a dedicated account for shared flows

Because the trigger follows the connection's owner, a flow built on a personal account stops working the day that person changes role or leaves. If the flow serves a team, connect it with a service account that everybody agrees to maintain, and document which account it is.

Watch the run history for a while before trusting it

Given the reports of the trigger going quiet, please do not build something critical on it and walk away. Let it run for a few days and check that the runs keep appearing. If silence would cause real harm, add a separate scheduled flow that checks for recent activity and raises a flag when the run history goes unexpectedly still.

Name it correctly

Always build the name so others can understand the trigger's purpose without opening it and checking the details.

Always add a comment

Adding a comment will also help avoid mistakes. Indicate what conditions trigger the flow and any assumptions about the data. It's essential to enable faster debugging when something goes wrong.

Final Thoughts

"When a new chat message is added" is the widest net the Microsoft Teams connector offers, and that width is both its appeal and its problem. It needs no setup, it reacts quickly, and it sees everything, but it hands you identifiers rather than messages, it cannot be narrowed at the source, and it watches exactly one person.

Use it when you genuinely want every conversation and you are ready to pay for a details lookup on each one. When you know which chat matters, reach for the scoped sibling instead and save yourself the loop, the extra call, and the noisy run history.

Sources

Back to the Power Automate Trigger Reference.

Photo by Miguel A Amutio on Unsplash

Comments

Spotted a mistake or have a better approach? Let me know. I read and reply to every one.

💬

No comments yet

Be the first to share your thoughts on this article!

Leave a Comment

All comments are reviewed for spam before being displayed 5000 left
Replying to