Capturing Praise in Teams with Power Automate

March 2021 update: I’ve finally got around to documenting the changes related to the change in format of the adaptive card.

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 PowerBI 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. Bear in mind that under the API call limit licensing model, your Office 365 seeded plan will allow you 2000 actions per day, so it’s important to stop the flow at the earliest opportunity.

I start by creating a Scope. I called it “establish whether this message is praise” and it contains four conditions:

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. The adaptive card in Praise (as of Nov 2020) now has 1 column, and it contains four items, and within one of those items there’s a text property that contains the string “sent praise to”.

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 fail.

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

These 4 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.

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 1 and the length of the “items” array within it is 5.

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.

The result of this is we will have an empty array if the string “sent praise to” does not exist in the text property of any of the items in the second column of the adaptive card.

The last action in this scope is to see if the array contains an object. If it does, then great, it was Praise, and if not, then it turns out it was just a Teams message with a mention in it and an adaptive card with one body and two columns.

Capture

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:

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

..and the Praise Type is

first(skip(first(outputs('AttachmentsContent')?['body'])?['items'],2))?['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:

38 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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s