Using Power Automate with Teams and Discourse

Wow!… So first of all, it’s been a long time since I’ve actually written an article (and a bit since I’ve created content on YouTube). Hopefully this year will include new fun projects, write-ups, and reviews!

Onto the current project at hand…

So I had a bit of a project goal of mine. I wanted to kickoff a user community forum, but I needed to make sure I had enough initial content to make it worth while to stay and contribute.

Internally, our employees have a large swath of discussions and data available in Microsoft Teams. Not all of this data is publicly consumable; however, a large portion could be incredibly valuable external facing. Often times we discuss new capabilities, neat tricks, or troubleshooting and resolution. Like most busy people, we often struggle with creating knowledge base (KB) articles in the moment and eventually the data gets lost to the horrible search that is Teams.

My idea was to automate flagged conversation threads and migrate them to a sanitized area within our Discourse forums. The entirety of the thread would be migrated, but at a later date I would have a singular spot to clean the data and re-categorize/tag as necessary.

So I guess Microsoft Power Automate?

Originally I looked at options for integrating Teams and Discourse together. Teams has it’s app extensions and natively offers connectivity with webhooks. Discourse has plugins (webhook notifications in Teams), as well as a specific Teams integration that I’m still not sure what I’m looking at. Additionally, other sites such as IFTTT.com offer webhook and API automation that has been able to fill the gap of missing plugins. Unfortunately, none of these did what I wanted. Push Teams messages to Discourse.

So I started looking at the “easiest” way to initiate automation based on user actions. This lead me to Microsoft’s Power Automate (formally known as Microsoft Flow).

Power Automate (PA) is a web, app, and teams app based platform that can trigger automation tasks. I learned this is probably the best way to describe the platform. “Trigger Automation Tasks”. The struggles are real the more in-depth of tasks you are looking to automate, but I’ll get into that in a bit.

The benefit of PA over other platforms [I thought], was the fact it’s directly integrated with Teams and there was little to no scripting required. PA offers a UI wizard for piecing together your actions, and with the native tie-ins, can initiate off of manual intervention or even things like keywords within specific channels or chats. And from the outside, this appears great.

Grabbing the trigger message and authentication

The easiest portion of the workflow is the trigger. Because of the native Teams integration, I could simply configure Channel or Chat, select the team or teams, and decide on a keyword(s) to monitor.

Next I needed to initialize the variables, which seemed a bit odd when you think of Windows automation (PowerShell). The initialize list took a bit of adjusting to fit the final workflow, but I was able to eventually find the perfect mix of vars required to store the various messaging data.

At this point I wanted to grab information about the thread that the trigger message was posted in. There is a simple “Get Message Details”, which worked fine for grabbing either the initiating message or the parent message being replied to. However, there didn’t seem to be a mechanism to identify the other messages in thread.

Researching online, there seems to be a long standing request for this functionality, but Microsoft feels REST calls to their Graph endpoints solves this. As I came to find out, Graph calls unfortunately over complicate the situation, especially when it comes to chat and channel messages.

In order to make these REST calls, I created an App Registration within the Azure portal and granted admin consent for the API endpoints required.

Note: The App client_id and tenant_id will be needed later. A secret will also need to be generated to authenticate as the application if desired.

  • Channel.ReadBasic.All
  • ChannelMessage.Read.All
  • ChannelSettings.Read.All
  • Team.ReadBasic.All

I found that there were two methods of granting these endpoints, either delegated, or application.

Application would be ideal from a programmatic perspective, but allows organization wide data to be retrieved. Additionally, the endpoints required for these actions are protected API calls, which are calls to overly sensitive private data such as conversations, email, etc. In order to allow me to use these API endpoints, I would fill out a Microsoft Form and every Wednesday Microsoft sits in some corner of their building and manually review these requests.

The second option would be delegate access. Delegate has the same rights as the user token being provided. Utilizing a service account (or user), I was able to retrieve my authentication token and pass that as a variable to my Graph calls.

The benefit of delegate is there are no wait periods and third party approval. The negative, is you must scope the user to any of the Teams in order to grant specific access. Also, if there are constraints around users and password rotation, this can be a bit of a pain to keep up on.

It was good, but not great…

Once I had the graph calls figure out, I struggled a bit with interpreting the data in PA. The “Apply to each” functioned nearly like a common For Each loop performed in any scripting language. It did throw me for a bit realizing I could only feed in specific outputs from previous steps and if I chose a var, such as my replyArray, I seemingly could not use that variable again within the loop.

Typically, in PowerShell I would do something like ForEach($reply in $replyArray) {}.

In the end, I found it a bit cumbersome that PA is all GUI driven, including the variable usages. I found that if I “peaked the code” or hovered over a variable, I could determine how something was actually written. This was incredibly useful for getting the Apply to each loops to work, but isn’t immediately apparent to new users.

Essentially, I had to create my own expressions and reference array data utilizing the ?[''] blocks. As seen in the example above, to reference the replyArray data for user ID, I had to type item()?['from']?['user']?['id']. Something I would’ve never guessed at until talking to others.

Overall, it took a bit to get the hang of, like most newly learned things.

Ideally, I would’ve liked to see an easier way to trigger a PowerShell script. There are some ways to do this, but they aren’t clean and require even more resources spun up. Since I already had to make calls manually to Graph, if I could’ve just used PowerShell the entire time, I would’ve been done in maybe 20 minutes instead of the 3 days I experienced.

There are definitely benefits to using PA due to the native tie-ins to Microsoft products, but they seem to be limited in use cases no matter the integration. Missing native actions for message threads seemed like a silly oversight, for example.

Other questionable limitations exist as well, such as the inability to sort. From a company that built it’s backbone off of dynamic sorting and pivot tables in Excel, I literally had to use a task to Excel to sort my array -_- This task was the cleanest way to approach this and required random code I found online to actually work.

Lastly, it’s scary modifying your workflow. Slight changes could break the flow even if the values were the exact same. I found a few times I had to delete then recreate the exact same object to correct issues at times. Also, trying to move the tiles of tasks/blocks around rarely worked. If you noticed a branching logic was required towards the beginning of the flow, you often times had to start over. -ugh-

The Entire Workflow

Here is the workflow in it’s entirety. It may not be optimal but it does the job just fine. Note, because Graph limits return to a max of 50, if you have threads longer than 50 replies, you will have to modify this to accommodate paging of the files.

Here I’m connecting to Teams and defining what Keyword and what Channels to monitor. The Random var is to generate random Titles to my posts to ensure no conflicts. Expression for random is: rand(1,99999)
Defining the additional variables I will need later
Note I’m able to reference the trigger message with the built in variables. I started with the initial thread post / “Reply to Message”. This way I could post the topic first to Discourse, then loop through providing the replies. I also do my best attempt to match the Teams user to the Discourse user. Unfortunately Discourse does not allow email matching in the API headers, so I was bound by username only. Because we utilize oauth the default usernames should match the email nickname in 0365.
Here I attempt to create the post topic. If this is successful, it goes to the left path. If it fails it retries on the right path, and posts as the generic “system” user.
The JSON schema had to be modified to allow nullable at several fields for this to work with the system user. Once a successful post occurs, I capture the topic_id from the return.
Now that I’ve created a successful post, I attempt to generate a user token to begin my Graph API calls. Since I’m using the delegated access I’m authenticating as a user to my tenant.
After a successful token grab, I can pass in my original message variables and dynamically grab the correct message thread. Oddly, the replies endpoint in Graph is not documented well. Also note, the top flag is set to the max allowed of 50 (before paging), but orderby is not allowed here.
Following the link posted in the article above, I had to sort the returning data by createdDateTime. Otherwise, I found that all of the posts happened in reverse order in Discourse. Unfortunately, even when supplying the created time to Discourse, the posts will not order themselves appropriately so you must submit in the correct sequence.
I set variables needed from the Apply to each loop and go the left path if user found, the right path if the user cannot match.
After the user is decided, I attempt to post the reply. The ability to correctly time stamp was nice so all of my messages didn’t appear as the exact same time to users. I also needed to remove posts that included my trigger word or were blank as they would fail the workflow (deleted messages for example).
This final step was optional, but I had it hear JUST in case the user submit didn’t work. The last thing I wanted was missing replies. Again, totally optional.

Enjoy the benefits!

Any questions on the workflow, I’ll be happy to supply answers. Hope others find this useful!

Be sure to check out https://youtube.com/codethethings for future DIY, reviews, and news!

You Might Also Like
Leave a Reply