Adding Notifications to MS Teams when Azure DevOps Work Items are Waiting using Power Automate

Benjamin Huser-Berta
11 min readNov 11, 2023

--

Are you using Azure DevOps and MS Teams? Would you like to improve your efficiency and be notified as soon as a Work Item is waiting for someone on your Board? Then keep reading, as I’ll show you how to use Power Automate to automatically post in your Teams Group Chat or Channel every time an Item is waiting for someone to be picked up.

At the end of this post, you know how to automatically post such cards to your MS Teams every time a work item is “waiting”

Wait Times?

Last week I was writing about how we inspected our Wait Times and how reducing them can have a big impact on efficiency.

As a first step, we agreed to manually post something in our MS Teams Group Chat as soon as an item is waiting. But as it’s a manual process, people might not think about it. So the logical next step was to look into automating this process.

Why Power Automate?

Nick Brown wrote three articles about how to use Power Automate to incorporate flow metrics related information on your Azure DevOps Board:

While we’re achieving similar things with Python scripts, I’ve wanted to try out Power Automate for a while. On top of that, connecting Azure DevOps with MS Teams is very well suited for a platform like Power Automate.

Prerequisites

There is only one prerequisite to set this up

As we’ll be using Azure DevOps Triggers, we’re tied to using a premium account, as otherwise they won’t work. If you do not have access, you can sign up for a 90-day trial.

General Flow

Before we start implementing, we should have a general idea of the flow of our Flow (projects in Power Automate are called Flows). Let’s break down what we would like to achieve:

  1. We want to get a notification in MS Teams.
  2. We want to get the notification when the work item is currently in a “waiting column”.
  3. We don’t want a notification if the work item is not in a “waiting column” or if it is but something else was changed
  4. We want this to happen automatically after the work item is updated

So far so good, that would lead to a flow similar to this:

Simplified Flow for the Power Automate Flow — Diagram created with https://app.diagrams.net/

Trigger

We start with a trigger — we want the flow to execute every time a work item is updated. For this, we can use the When a work item is updated flow from the Azure DevOps group:

Specify your organization, project, as well as your team or Area Path if you need to filter for it. Once done, you’ll flow will run every time you update an item:

Enter the details to trigger your flow every time a work item is updated.

Getting the Current Board Column

The next step is to read the current column the item that was updated is in. We only care about it if it’s currently in a “Waiting for” column, so we need that value. Luckily we can get that information from the output of the trigger. For this, we initialize a new variable:

Use the “Initialize Variable” action to read out specific values from the Output of the Trigger

The Board Column is stored in a field called System.BoardColumn. In the output of the trigger, the ‘.’ is replaced by an ‘_’. So we can fetch the value by accessing the field as follows via an expression:

triggerOutputs()?['body/fields/System_BoardColumn']

The fully configured variable looks like this:

Initialization of the BoardColumn Variable

Issues with the BoardColumn Field

When I started with my flow, I experienced the problem that the System.BoardColumn was not set correctly. Instead of the proper names of our workflow stages, it would display the states like New, Active, and Resolved.

After some googling, it turned out that this happens if multiple teams use the same area path. We had some “dummy teams” using the same one, which creates a board for this team with the default columns that are equal to the states. After I removed the area path from that team, the board column field was properly populated.

See also the discussion in the Microsoft Developer Community for more information:

Make Sure the Board Column was Changed

At this point, we know that an item was updated recently and in what column in our workflow it currently is. However, maybe someone changed the description or added a comment — in that case, we don’t want a notification. So we need to check whether the Board Column was changed or it was another change.

Get Updates

First, we’ll get the updates that were made to this work item. We can use the “Send an HTTP request to Azure DevOps” action for this.

Send a generic REST Request to Azure DevOps with this action

The action allows us to do generic requests to Azure DevOps. Enter your organization name and pick GET as the Method. The URI to send the request to looks like this:

@{triggerOutputs()?['body/fields']?['System_TeamProject']}/_apis/wit/workItems/@{triggerOutputs()?['body/id']}/updates?api-version=5.1

This will send a request to retrieve all the updates made to this work item. The team project and work item ID are dynamically filled in based on the work item that was updated.

Which Updates to Include?

With the above request, we get all updates made to this work item, not exactly what we want. When I started, I assumed we could just check the latest update, but this is also not correct — the flow will not trigger for all updates but is executed every 3 minutes (a setting that can be changed in the trigger).

Trigger Frequency

This means we need to check all the updates within the last 3 minutes, and if the board column was changed in one of those updates, we consider creating a notification.

Filter Updates

To filter the updates, we can check the revisedDate field of the updates. If the update was in the last 3 minutes, we consider it.

Let’s start by initializing a variable that includes the “Time Horizon” we want to check. We set the value to “3 minutes ago” right after the flow was triggered:

getPastTime(3, 'Minute')
Get Time 3 Minutes before the flow was triggered

Next, we want to filter the results based on this. For this, we first need to parse the results containing all the updates to JSON:

Parse JSON Action

As Content, you use the Body from the Get Request before. The schema can be generated by a sample. Just copy and paste the example response from the documentation:

Parse JSON Action Configured with content and Schema

Now that we have a JSON array, we can filter it using the Filter Array action:

Filter Array Action

This action requires two inputs: The initial array and a filter query. Use the Body value from the previous step as input:

Variables from Parse JSON step

For the query, we want to check every update and see if it was made within our “Time Horizon”. The Filter Query follows this pattern: Value 1 compared to Value 2.

For the first value, add the following expression:

parseDateTime(item()['revisedDate'])

This will go through every item in our array read out the revisedDate field and convert it into a DateTime.

As a comparison, choose is greater or equal to.

For the second value, we use our previously generated variable:

parseDateTime(variables('RelevantTimeHorizon'))

The result will look something like this:

Filter Action Configured

Check If the Board Column Has Changed

The hard part is done, we’ve got all the updates that were made to the work item in the last 3 minutes. Now what’s left is to check if the BoardColumn was updated.

Initialize a new variable of type Boolean and add the following expression as a value:

contains(string(outputs('Filter_Changes')), 'System.BoardColumn')

We’re using the output from our previous step (here named ‘Filter_Changes’ — use the UI to add dynamic content in the expression to make sure you’ll have the right value). We can keep it simple and convert the filtered array to a string, and simply check if it contains the field we’re interested in: “System.BoardColumn”.

If yes, it means the board column has changed.

Check Conditions

By now we have all the relevant information to decide whether we care about this update or not. To do so, we can add a Condition action:

Condition Action

When you configure it, you can set the expression that either evaluates to true or false. In the false case, we don’t want to do anything, and in the true case, we want to send the notification.

We’re using two variables for the check: BoardColumn and BoardColumnChanged.

The expression for you might be different — we’re using column names that start with Waiting for xxx. So we want to send a notification if:

  • The BoardColumn has changed
  • The current BoardColumn starts with Waiting for

On top of that, we want to ignore if an item is currently in Waiting for Prioritization. This is because that’s our initial column and it’s not part of our definition of workflow.

The final configuration looks like this:

Condition to see if we want to take any action or not

Send Notification in Teams

If the above condition evaluates to true, we want to send a notification. Various actions can be executed in Microsoft Teams. You can send simple messages, and post to group chats or channels. We went with the Post card in a chat or channel action and post an Adaptive Card to our group chat we use in the team:

Post Card Action

Adaptive Cards

Adaptive Cards are defined via JSON and will be rendered “nicely” in MS Teams. They allow to not only display text but also have actions associated with them via buttons.

There is a designer that can help you design your card:

Designing our Card

In our example, we’re posting the following Card:

In our example, we’re posting the following Card:

As you can see we have:

  • The title that is combined of the work item title and the current board column (blacked on the screenshot)
  • A highlighted column with the board column
  • Who made the last change to the item (blacked on the screenshot)
  • The current work item age
  • A button to directly open the item

To achieve this, we need to initialize one more variable to get the “Last Changed By” name. As it contains the name including the email address in the format Display Name <Email Address>, we need to split it up:

split(triggerOutputs()?['body/fields']?['System_ChangedBy'], '<')[0]

This will get the content of the ChangedBy column, split it on the first occurrence of <, and will take the part of the string before the occurrence.

Once we’ve done this, we can use the following JSON to describe our card:

{
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"size": "medium",
"weight": "bolder",
"text": "@{triggerOutputs()?['body/fields']?['System_Title']} is @{variables('BoardColumn')}"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"text": "📢 @{variables('BoardColumn')} 📢",
"wrap": true,
"color": "Attention",
"weight": "Bolder"
},
{
"type": "TextBlock",
"text": "Last Changed by: @{variables('LastChangedBy')}",
"wrap": true
},
{
"type": "TextBlock",
"spacing": "None",
"text": "Work Item Age: @{triggerOutputs()?['body/fields']?['Custom_WorkItemAge']}",
"isSubtle": false,
"wrap": true
}
],
"width": "stretch"
}
]
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "View @{triggerOutputs()?['body/fields']?['System_WorkItemType']}",
"url": "https://dev.azure.com/<YOUR ORGANIZATION>/@{triggerOutputs()?['body/fields']?['System_TeamProject']}/_workitems/edit/@{triggerOutputs()?['body/id']}"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.4"
}

All dynamic fields are taken either from the trigger or from variables we defined in the process.

Important: We store the work item age in a custom field called WorkItemAge. That’s why we access it via Custom_WorkItemAge. If you don’t have this field defined or named differently, you want to adjust the JSON accordingly.

And that’s it, next time you move a card to a Waiting For column, this card will appear in the chat or channel you specified.

Individuals and Interactions over Processes and Tools

One important thing to mention is that the notification in MS Teams helps us trigger discussions. They make it easier for the team to focus on items that are waiting. It shall not replace the interaction by having a process in place. If an item is waiting, it shall be brought to the whole team’s attention, so we can figure out how to move the item along as quickly as possible.

Processes and Tools can be supportive of Individuals and Interactions — Source: Agile Manifesto

It’s not meant to be a “handover” tool. On the contrary, it’s helping us to break up silos as we realize the regulars might not have time to move that item out of the current column, someone else can jump in. The notification helps us to enable interactions.

So if you are toying with the idea of implementing a similar notification mechanic, you should make sure you and your team are aligned on this and how to reduce Wait Times.

Conclusion

In this post, you learned how to link Azure DevOps Work Item Updates to MS Teams using Power Automate. You saw how you can connect to both Azure DevOps and MS Teams with existing actions in a Power Automate Flow and how you can parse and filter responses.

As a first-time user of Power Automate, I’m quite impressed by how simple it is to get started. But the simplicity doesn’t mean it ain’t powerful, as you can do lots of more elaborate actions. If you’re working in the Microsoft Universe, Power Automate might be worth a look to optimize your workflows. Be it to support your team by posting notifications on teams to support interaction or to fully automate other workflows.

And while the automation on its own will not be a silver bullet to reduce your wait times, it can be a good assist to you and your team:

--

--