Capturing Praise in Teams with Power Automate

July 2023 update: The adaptive card format changed some time ago, but it was a very easy fix and I forgot to update the blog. I’ve also created a power automate package to download. It’s a fully featured flow with the core functionality of detecting prise and storing it, but also it responds to the praise in Teams and notifies in another channel if something breaks, which it does every year or two.

Skip to the end if you don’t care how it works and just want the package.

There isn’t a trigger for Praise in the Teams connector in Power Automate (UserVoice here), but tracking praise seems like quite a handy thing, if not for an HR department or team leader to do, but for yourself personally.

Imagine going into your next performance appraisal with a Power BI report that not only shows how much praise you give to and get from your colleagues, but also demonstrates your Power Platform skills!

Imagine no longer, because I’m going to show you how to do this in Power Automate.

Firstly the trigger. This is quite simple, we trigger on every new message in a channel.

Capture.PNG

Next we need to filter out what isn’t praise and terminate the Flow.

I start by creating a Scope. I called it “establish whether this message is praise” and it contains two conditions, the presence of attachments and mentions:

Capture.PNG

A standard message has empty arrays for attachments and mentions. Praise has both (one attachment and one or more mentions), so unless the message has both attachments and mentions, it’s definitely not praise, so end here.

Now we need to establish whether the attachment is an Adaptive Card, and if not, end the flow. We get this information from:

first(triggerBody()?['attachments'])?['contentType']

The value should be “application/vnd.microsoft.card.adaptive”

Capture

Next we need to do something with the first attachment’s content. If you look at the raw JSON output from the trigger, the attachments content is a serialised JSON string:

Capture.PNG

If we select that property with

first(triggerBody()?['attachments'])?['content']

then the output is a string. It’s still serialised and we need to deserialise it with the json() function. The final expression in a Compose should be

 json(first(triggerBody()?['attachments'])?['content'])

Now, the next set of conditions are a little trickier.

There are arrays within arrays within arrays in the Adaptive Card JSON so we need to play with expressions and data operations a bit here.

Firstly though, while Praise comes on an Adaptive Card, it’s not the only thing that might be shown on one. The subsequent operations use expressions that rely on certain characteristics being present in the adaptive card, and if they’re not, the flow will terminate gracefully.

The Praise Adaptive Card has a body, which is an array that contains two items (a Container in Adaptive Card parlance), which has two properties, type and items. Type is “container” and items is an array with some objects inside.

These objects are the details of the praise: Who sent it, who received it, the image that relates to the type of praise it is, and the text description, as written by the person giving the praise, that sort of thing.

The contents of these objects is how we identify whether this adaptive card is actually praise.

To be able to get to that point, we need to check the length of the body array is 2 and the length of the “items” array property within the first one of those two “body” items is 7.

The screen shot below shows three data operations; a Compose and two Filter arrays.

The expression in first body is

first(outputs('AttachmentsContent')?['body'])

Here is the screen shot. The “From” input of the first Filter array action (Filter items for TextBlock) is the expression outputs(‘first_body’)?[‘items’] and the input of the second one (Filter items for text containing praise) is just the output of the action above it.

In a previous version of this, we’d look for the string string “sent praise to” in the property called text. The result of this is we had an empty array if the string “sent praise to” did not exist in any of the text blocks.

We can’t do that any more, but a way we can sort of reliably identify it being praise is to check the spacing property. There’s only one which equals “large” so the result should have a single item in the array

The last action in this scope is to see if the array contains an object. If it does, then great, it was probably Praise, and if not, then it turns out it was just a Teams message with a mention in it and an adaptive card with two items in the body, the first of which has an array property with 7 items, exactly one of which is a text block with the spacing set to large.

Capture

I’m sure there’s a more elegant way to do this but this has been working 100% reliably with no false positives or negatives for years, expect when Microsoft change the adaptive card schema.

Anyway, now we’ve established it’s Praise we can do something with the data.

Another scope:

In the Apply to each is the ‘Message mentions’ dynamic content from the trigger. You can do several mentions in one Praise so we want to create a list item for each of them.

Within the loop I do a lookup on the user. The mentions in the Teams message only has the Azure AD ID of the user, which is like a GUID. Thankfully the Office 365 Users connector takes that as an input (despite it asking for a UPN!).

The title is the personalised message text, which is:

first(outputs('AttachmentsContent')?['body'])?['items']?[4]?['text']

..and the Praise Type is

first(skip(first(outputs('AttachmentsContent')?['body'])?['items'],1))?['altText']

You could also capture the image URL I guess, but I haven’t done that.

Lastly, I do a few follow up actions.

I produced a report of the praise by connecting Power BI to this SharePoint list. So outside of the Apply to each loop, I want to refresh my Power BI dataset after new praise is added.

I’m also going to follow up the praise by replying to the Praise message in Teams with a count of the praisers praise this year. This shows me the process is still working (note the total lack of any error control in this) and reminds people of the existence of the Power BI report:

That’s it folks. This is one of those simple but also quite tricky flows. It’s simple because it’s just catching a small number of bits of data from a Teams message, but tricky because the structure of the adaptive card is slightly complex and for the flow to run each time without errors, it needs to cleanly terminate at a number of places before any data runs through a formula that can’t take that data as an input.

As always, I hope someone finds this useful, and comments (or indeed some praise!) are welcome. If you feel my work has saved you heaps of time and effort, I will gratefully accept a small donation via PayPal:

Now, if you just want the file, it’s here.

You will need to update the SharePoint site URL at the top of the flow to your one, and update the Teams, SharePoint and Power BI actions to point to your lists, libraries, Teams & channels and your Power BI dataset.

The list which contains the data needs columns as per the screenshots above. I don’t have a lot of time to support this beyond answering quick questions so you if you just need it put in your environment and got working and you don’t have a clue how to start, then please engage a consultant 😉. You can point them to this page if you like.

47 Comments

    1. Good grief, it’s Nick Bergquist! With tools like Power Automate and Power Apps I can just about pretend to be a real developer like you Nick. 🙏

      Like

  1. This article is really useful. I am trying to complete the flow, I have added another scoop (do something with praise) but when I add the action “Get User Profile” I have no “mentioned.user” option and I can not follow creating an item. Could you please advise?

    Like

    1. Hi Erica. You can use this expression there: items(‘Apply_to_each’)?[‘mentioned’]?[‘user’]?[‘id’]

      Like

      1. Thank you so much Will, worked! Now I am testing the flow but is cancelled. How did you manage to add the text property of the output in the “filter items for text containing praise”? I don’t know how to do it and I am only getting empty arrays as a result.

        Like

      2. Hi Erica. That’s a Filter Array action. The input is the result of the step above it; outputs(‘last_column_items’) and the filter is item()?[‘text’] contains sent praise to. In advanced mode the expression is @contains(item()?[‘text’], ‘sent praise to’)

        Like

      3. i tried to use this expression on the module get user profile (v2), but i’m getting the error: WorkflowOperationParametersRuntimeMissingValue. the ‘inputs.parameters’ of workflow operation of type OpenApiConnection is not valid. they may not be null or empty ‘id’

        Like

      4. It’s hard to say without seeing your flow, but the expression above will only work within an Apply to each loop that hasn’t been renamed. You mileage may vary.

        Like

      5. It’s hard to help without seeing what you’ve done. If you’re sure the channel will only contain praise you can skip a lot of the previous scope, but you will still need to parse the JSON and do some processing from the first scope. Include the Compose action called AttachmentsContent, the Parse JSON action and the Compose called ‘last column items’ from the previous scope. You will get failed flow runs if any non-conforming messages are posted to the channel though.

        Like

  2. if you are positive that you are going to use this channel in teams only for praise, can you jump directly to “do something with the praise”, I tried, but I’m getting errors with the expressions.

    Like

  3. thanks for the fast reply, do you have an email account to send you the screenshots of my flow?. now i’m getting the error in the flow checker: “correct to include a valid reference to ‘last_column_items’ for the input parameter (s) of action ‘create_item’

    Like

  4. this page is very useful.
    I am dealing with the same error as Erica for the data compose called ‘filter items for text containing praise’. In advanced mode i added the code: @contains(item()?[‘text’], ‘sent praise to’)
    But I believe the error is caused by the inability to locate ‘text’. The error might then be with the Parse JSON. Nothing is filled in its schema as is described (if i add: {} , it will auto remove it).
    Any idea what the error could be?

    Like

  5. maybe i still dont fully understand what is going on, but this is how i solved the issue. In between the ‘last column items’ and the Filter array, I added a second Parse JSON. I added a schema generated from sample. The sample is just the output from ‘last column items’. Then in the filter array i can refer to ‘text’ and filter correctly.

    For some reason doing something similar with the first Parse JSON did not work. I believe that should work, but i got it working this way.

    Like

    1. That’s good to know. All Parse JSON does is expose dynamic content based on the schema, to use later in the flow. You don’t really need it if you know the schema, because you can just manually code the path to the property you want using the expression builder. I don’t really know why I bothered with the Parse JSON action in this guide, it could have just been a Compose.

      The trouble with Parse JSON is if the input doesn’t conform to the schema it errors out. Glad you got it working.

      Like

  6. Hey! Pity I’ve only come across this now. I’m a newbie to coding and Power automate, so haven’t been able to figure out how to do this without resorting to others’ flows. Would love to see your update! And hopefully it’ll be accessible enough for me to bring this to life for my team — I think if I can combine this with some custom badges, we’ll be in for a good ride.

    Like

  7. This is an awesome post and very helpful. I was trying to create this feature but having issue when tried to run it. My error is “The ‘from’ property value in the ‘query’ action inputs is of type ‘Object’. The value must be an array.” This error happen in “Action ‘Filter_items_for_TextBlock’ “.

    I could not pass an array to ‘Filter_items_for_TextBlock’ and i can only get “output” but not “items” from “first body”.

    Can you help me?

    Like

    1. Whatever expression you have as the input for that filter array action, put it in a compose (above the filter), run the flow and paste the expression you used and the output of the compose here.

      Like

      1. I got the same issue, as Freedom Fighterz with the same error message, but can’t solve it with the proposed solution. Could you specify please a bit more? I do not get it.

        Like

  8. Ah okay, thanks Will. Here we go:

    {
    “items”: [
    {
    “horizontalAlignment”: “center”,
    “isSubtle”: true,
    “text”: “XXXX sent praise to”,
    “wrap”: true,
    “type”: “TextBlock”
    },
    {
    “horizontalAlignment”: “center”,
    “size”: “large”,
    “text”: “YYYYY”,
    “weight”: “bolder”,
    “wrap”: true,
    “type”: “TextBlock”
    },
    {
    “altText”: “Courage”,
    “horizontalAlignment”: “center”,
    “url”: “https://statics.retailservices.teams.cdn.office.net/ui/static/praise/master/all/assets/badgesV2/en-GB/CourageBadge.e4d32c13a3.png”,
    “width”: “124px”,
    “height”: “auto”,
    “spacing”: “medium”,
    “type”: “Image”
    },
    {
    “horizontalAlignment”: “center”,
    “size”: “large”,
    “text”: “”,
    “wrap”: true,
    “spacing”: “medium”,
    “type”: “TextBlock”
    }
    ],
    “type”: “Container”
    }

    Like

    1. Indeed, you found a mistake in my instructions. The input of “Filter items for TextBlock” should be the expression: outputs(‘first_body’)?[‘items’] not simply the output of the action above it. I have updated the article to clarify that.

      Like

  9. Will, any possibility you can give me a full walkthrough on this? You are the only person who has created a step by step instruction but i am not able to follow along with your instructions. I wish i was a developer or someone who can understand the above, but im not. Could you assist, maybe via email with a more detailed set of instructions?

    Like

      1. Hey Will, I’m in the same boat as Frank. Any way to get what you sent to him with more detail or walkthrough? Thanks

        Like

      2. Hi Joe. Send me a private message and I’ll reply by email. I sent Frank an export of the whole flow and the Power BI report it was built from, but didn’t get so much as a thank you in response. Anyway, his rudeness notwithstanding, I can forward you what I sent him.

        Like

      3. Will, I read a previous comment about not giving you a thank you In response and how it was rude. I apologize I haven’t as my day to day has caught up with me. Here is my thank you. Your process for Praise has been absolutely helpful for my organization and is being used daily. You made this simple and very helpful. I am forever grateful for your help. Again, apologies I never formally thanked you. What you did for me is highly appreciated.

        Liked by 1 person

  10. I have a couple questions pertaining to the last Scope of the flow. Specifically the Refresh a dataset action for PowerBi as I am uncertain if any preparations are needed to make a compatible dataset. I am familiar and rather skilled at PowerApps but still a newbie to PowerBi. Also could you please show what dynamic content you used in the Filter for giver action as their full names are cut short.

    Like

    1. If you have created a Power BI report using the SharePoint list as a data source and published it to a workspace in Power BI cloud service, you can use that action to refresh the dataset. It’s not necessary if you have not published a report to the web or you would rather refresh the dataset on a timer. Power BI Pro license only allows up to 8 refreshes a day so if the amount of praise being posted to your Teams channel is likely to exceed this, you are better off setting a schedule instead of refreshing via the API using the Power Automate action.

      The Filter for giver action is filtering the “value” property of the Get items action above it and the filter condition is “From DisplayName” is equal to “Message from user displayName”. The expression (advanced mode) looks like this: @equals(item()?[‘From’]?[‘DisplayName’], triggerBody()?[‘from’]?[‘user’]?[‘displayName’])

      Like

  11. The flow was working perfectly till mid of February 2022. After a recent update, it terminates by the first condition. There is no error, just cancels the flow, with “no input”. The output from the previous step looks fine, see below. (please note I replaced the names, IDs with xxxx). Do you have any advice, how to revive it?

    {
    “@odata.type”: “#microsoft.graph.chatMessage”,
    “etag”: “1646216862473”,
    “messageType”: “message”,
    “createdDateTime”: “2022-03-02T10:27:28.808Z”,
    “lastModifiedDateTime”: “2022-03-02T10:27:42.473Z”,
    “importance”: “normal”,
    “locale”: “en-us”,
    “webUrl”: “https://teams.microsoft.com/l/message/19%3A5137973aa5c44cd99d32d94e708211be%40thread.skype/1646216848808xxxxxx
    “id”: “1646xxxxxxxx”,
    “from”: {
    “user”: {
    “id”: “4d4d1b55-4e14-4178-8cd0-95xxxxxx”,
    “displayName”: “Nemeth,xxxxxx (uib0xxxxxx)”,
    “userIdentityType”: “aadUser”
    }
    },
    “body”: {
    “contentType”: “html”,
    “content”: ” Korim, xxxxxx (uixxxxx) Tomko, xxxxxx (uixxxxx) ”
    },
    “channelIdentity”: {
    “teamId”: “470b2a22-114b-4c8d-882d-5xxxxxx3”,
    “channelId”: “19:5137973aa5c44cd99d32d94e708211xxxxxxxype”
    },
    “attachments”: [
    {
    “id”: “deea5ef098394989a6dxxxxxxxxe1a”,
    “contentType”: “application/vnd.microsoft.card.adaptive”,
    “content”: “{\r\n \”type\”: \”AdaptiveCard\”,\r\n \”body\”: [\r\n {\r\n \”items\”: [\r\n {\r\n \”horizontalAlignment\”: \”center\”,\r\n \”isSubtle\”: true,\r\n \”text\”: \”Nemeth, xxxxxxx(uibxxxxxx) sent praise to\”,\r\n \”wrap\”: true,\r\n \”type\”: \”TextBlock\”\r\n },\r\n {\r\n \”horizontalAlignment\”: \”center\”,\r\n \”size\”: \”large\”,\r\n \”text\”: \”Korim, xxxxxx (uidm5709), Tomko, xxxxxxxx (uixxxxxx)\”,\r\n \”weight\”: \”bolder\”,\r\n \”wrap\”: true,\r\n \”type\”: \”TextBlock\”\r\n },\r\n {\r\n \”altText\”: \”Creative\”,\r\n \”horizontalAlignment\”: \”center\”,\r\n \”url\”: \”https://statics.retailservices.teams.cdn.office.net/ui/static/praise/master/all/assets/badgesV2/en/CreativeBadge.3ecfe7ef6b.png\”,\r\n \”width\”: \”124px\”,\r\n \”height\”: \”auto\”,\r\n \”spacing\”: \”medium\”,\r\n \”type\”: \”Image\”\r\n },\r\n {\r\n \”horizontalAlignment\”: \”center\”,\r\n \”size\”: \”large\”,\r\n \”text\”: \”Thanks for this praise-wall proposal and introduction to the location! :)\”,\r\n \”wrap\”: true,\r\n \”spacing\”: \”medium\”,\r\n \”type\”: \”TextBlock\”\r\n },\r\n {\r\n \”text\”: \”**[Review your praise history](https://teams.microsoft.com/l/entity/57e078b5-6c0e-44a1-a83f-45f75b030d4a/MyAssist?context=%7B%22subEntityId%22%3A%22%7B%5C%22PageUrl%5C%22%3A%5C%22%2FPersonalApp%2FHome%2FPraise%2F%5C%22%2C%5C%22Queries%5C%22%3A%5B%7B%5C%22Name%5C%22%3A%5C%22Source%5C%22%2C%5C%22Value%5C%22%3A%5C%22PraiseClinet%5C%22%7D%5D%7D%22%7D)**\”,\r\n \”spacing\”: \”extraLarge\”,\r\n \”separator\”: true,\r\n \”type\”: \”TextBlock\”\r\n }\r\n ],\r\n \”type\”: \”Container\”\r\n }\r\n ],\r\n \”$schema\”: \”https://adaptivecards.io/schemas/adaptive-card.json\”,\r\n \”version\”: \”1.1\”\r\n}”
    }
    ],
    “mentions”: [
    {
    “id”: 0,
    “mentionText”: “Korim, xxxxxx (uixxxxxx)”,
    “mentioned”: {
    “application”: null,
    “device”: null,
    “conversation”: null,
    “tag”: null,
    “user”: {
    “id”: “a8d53acd-e5e9-40c4-9d61-e2d5d71ce2d2”,
    “displayName”: “Korim, xxxxxx (uixxxxxx)”,
    “userIdentityType”: “aadUser”
    }
    }
    },
    {
    “id”: 1,
    “mentionText”: “Tomko, xxxxxxxx (uixxxxxx)”,
    “mentioned”: {
    “application”: null,
    “device”: null,
    “conversation”: null,
    “tag”: null,
    “user”: {
    “id”: “074ae22a-f911-4d0f-8d48-781a485a73d9”,
    “displayName”: “Tomko, xxxxxxxx (uixxxxxx)”,
    “userIdentityType”: “aadUser”
    }
    }
    }
    ],
    “reactions”: [
    {
    “reactionType”: “like”,
    “createdDateTime”: “2022-03-02T10:27:42.502Z”,
    “user”: {
    “application”: null,
    “device”: null,
    “user”: {
    “id”: “3ef00030-f96e-4d8d-ba60-f48d661a60b3”,
    “displayName”: null,
    “userIdentityType”: “aadUser”
    }
    }
    }
    ]
    }

    Like

    1. Issue fixed, JSON content has now 5 elements instead of 4. Besides, triggeredOutput(attachment) should be used.

      Like

  12. Hello! I stumbled upon this this week and am SUPER thankful for you explaining how to do this. This was actually my 1st time using Power Automate and I was actually able to set it up correctly after lots of trial and error. However, I can’t seem to figure out what to put for the “Card Type Id” field within the “When someone responds to an adaptive card” trigger. A little backround info: We are looking to prompt people to send Praise on a Friday and would start with an example Praise and would prompt the members of the Teams Channel to send a Praise to someone within the replies for that vs. individual posts as it would create way too much noise in the channel which is mainly used for announcements only. Would you be able to help me with this?

    Like

    1. I missed this comment – I expect you’ve managed it already by now, but anyway.. I’ve never used that trigger before, but I expect you’ll either need to have a look at a past flow run of the flow that posts the card in the first place to see if that is part of the output of the action that posts the card, or if not, then it might be something you need to include in the payload when you post the card in the first place. A GUID perhaps, which gets included in each response. I can’t say much more than check the documentation I guess!

      Like

  13. Hi Will. This flow has been working great. As of 11/28 I noticed that MS changed the format of the adaptive card again (thanks MS). I don’t suppose you could help a lowly IT guy like myself to get this back working again? Thanks!

    Like

      1. Hello, I downloaded the .zip file but received an error when trying to import the solution. How can I get it going?

        Like

  14. I’m having problems with the “does it have attachments or mentions” scope, it is coming up with the following error:

    “InvalidTemplate. Unable to process template language expressions for action ‘does_it_have_attachments_and_mentions’ at line ‘0’ and column ‘0’: ‘The template language function ‘length’ expects its parameter to be an array or a string. The provided value is of type ‘Null’. Please see https://aka.ms/logicexpressions#length for usage details.’.”

    I have tried looking on the site it suggests but can’t find how to rectify, are you able to help?

    Like

Leave a comment