Sending messages to MS Teams — a python script

Saurabh
5 min readJan 28, 2025

--

A few months back, I was forced by Microsoft to abandon time-tested procedures for getting messages from our production cluster and application to our MS Teams channels in favor of their new, ill-documented, perhaps ill-conceived, way of doing things. Looking back, after calming down and finding solutions, the outcome wasn’t too bad (looking at you, Bill).

The new procedure required a separation into two distinct flows — a webhook is defined and then you could either send it a message as such, in text form, or you could send it as a preformed adaptive card. The webhook is linked to a Power Automate Flow which is responsible for either passing on the input as is, or wrapping it in an adaptive card and then sending it forth.

While the choice was not quite as dramatic as the one Morpheus offered to Neo, it still had to be made.

Image generated using ChatGPT and referencing the iconic Matrix moment, in case you didn’t notice.

Many of our messages at Koverhoop are preformed adaptive cards created by the sender, for example a lambda function reporting on the GoKlaim website URL accessibility or a periodic check of infrastructure. However, information coming from the cluster via the Prometheus-Alertmanager route is in the form of direct text content. So here, the Power Automate flow needs to be tasked with creating the Adaptive card before sending it to the MS Teams channel. I have documented the story in another blog, should you still be stuck in the doldrums on that boat.

In the early days of navigating these waters, I needed to send zillions of test messages, just to make sure my ship did not run aground. And herein lurks the true test of a developer’s soul. If you can do something through the GUI, but there is also that window of opportunity to automate it somehow, no developer worth their salt would give up that chance.

So, the need of the hour was to develop a script that would send messages of either ilk into a channel of our choosing, all from the comfort and familiarity of that wonderful place called the command line. Remember, all this was happening even as winter was setting in. The implications of this little factoid were clear — every time one pressed a few keys on the keyboard and the task was done, one saved on considerably more hand and finger movements than would have been necessary had the GUI route been taken. The sheer ergonomics were game-changing for wintry cold hands.

Image generated using ChatGPT and prevailing atmospheric vibes

With that icy backdrop, let’s break down the script components. The full script itself is at the bottom of the blog, because these other parts are light reading and being so light, they naturally rose to the top.

Part 1 — the customizable bit

As a script, the whole thing is customizable, but this part is more fluid than the rest. A virtual cup of hot chocolate. I refer to selecting the webhook where the message is to be delivered. Within the same segment of code are slightly more durable elements — the nature of the message and the name of the workflow. These are all wrapped up in a nice dictionary structure called the webhook_mapper, which includes an index that can be used to pick up whatever is needed. The index is called idx, a reflection of how cold it really was and how each keystroke needed to be fully optimized.

Part 2 — the fixed bit

This contains everything that did not change from run to run. So at the risk of incurring the wrath of Python purists, I placed the two library imports here, well into the meat of the lines-of-code. This was followed by extraction of information from the webhook_mapper, based upon the idx value.

Two global variables are defined next. Both of these contain material that is sent to the MS Teams channel.

  1. header_text
  2. message_body

Then come three function definitions. The functions themselves are absolutely uncomplicated and therefore I will save further keystrokes by not explaining how they do what they do. Just take a peek at the code that I have shared further on.

  1. create_adaptive_card
  2. send_adaptive_card_to_ms_teams
  3. send_message_to_ms_teams

And finally, an if-elif block to use everything else and figure out what to send and where.

With this script, I just need to change the value of idx, which is placed at the very top and requires no scrolling whatsoever. Please do appreciate the attention to ergonomics here. This is the reason that doctors, who care for one’s health and comfort, are the best persons to write code.

Here is the full script.

#--CUSTOMIZE HERE ---------------------
idx = 2
webhook_mapper = {
1: {"workflow_name":"staging-alertmanager",
"message_type": "Direct Message",
"webhook": "https://prod-23.xxx.logic.azure.com:443/workflows/xxxxx/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=xxx-xxx-xxxxx"},
2: {"workflow_name":"informational direct message workflow",
"message_type": "Direct Message",
"webhook": "https://prod-23.xxx.logic.azure.com:443/workflows/xxxxx/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=xxx-xxx-xxxxx"},
3: {"workflow_name":"informational adaptive card",
"message_type": "Preformed Adaptive Card",
"webhook": "https://prod-23.xxx.logic.azure.com:443/workflows/xxxxx/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=xxx-xxx-xxxxx"},
0: {"workflow_name":"",
"message_type": "",
"webhook": ""},
}

#--NO CHANGES REQUIRED BELOW THIS LINE - YESSS !!
import urllib3
import json
workflow_name = webhook_mapper[idx]["workflow_name"]
message_type = webhook_mapper[idx]["message_type"]
webhook = webhook_mapper[idx]["webhook"]
header_text = f"Testing MS Teams Workflow with {message_type} sent to {workflow_name} webhook."
message_body = "test message body"

#--FUNCTION DEFINITIONS
def create_adaptive_card(header_text,message_body):
'''
create and return an adaptive card
'''
adaptive_card = {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{
"type": "TextBlock",
"text": header_text,
"style": "heading",
"size": "Large",
"weight": "bolder",
"wrap": True,
"color": "good"
},
{
"type": "TextBlock",
"weight": "default",
"wrap": True,
"size": "default",
"text": message_body
},
]
}
return adaptive_card

def send_adaptive_card_to_ms_teams(webhook,adaptive_card):
'''
send an adaptive card to an MS Teams channel using a webhook
'''
http = urllib3.PoolManager()
payload = json.dumps(
{
"type": "message",
"attachments": [{"contentType": "application/vnd.microsoft.card.adaptive", "content": adaptive_card}]
}
)
headers = {"Content-Type": "application/json"}
response = http.request("POST", webhook, body=payload, headers=headers)
print("response status:", response.status)
if response.status>=300:
print(response)

def send_message_to_ms_teams(webhook,message_body):
'''
send a simple text message to an MS Teams channel using a webhook

The webhook receives a simple message,
the Power Automate workflow creates an adaptive card and posts to MS Teams
Ensure that the Power Automate workflow has been so configured.
'''
http = urllib3.PoolManager()
payload = json.dumps(
{
"@context": "http://schema.org/extensions",
"type": "MessageCard",
"title": "Direct Message Testing",
"summary": "This workflow accepts a direct message rather than a preformed adaptive card",
"text": message_body,
"themeColor": "2DC72D"
}
)
headers = {"Content-Type": "application/json"}
response = http.request("POST", webhook, body=payload, headers=headers)
print("response status:", response.status)
if response.status>=300:
print(response)

# THE IFS AND THE ELSES
if message_type == 'Direct Message':
print("Direct message")
send_message_to_ms_teams(webhook,message_body)
elif message_type == 'Preformed Adaptive Card':
print("Preformed adaptive card")
adaptive_card = create_adaptive_card(header_text,message_body)
send_adaptive_card_to_ms_teams(webhook, adaptive_card)

Feel free to use as required. The script is not optimized because as soon as it started working, I stopped fiddling. Perhaps the idx could be passed as a CLI runtime argument, or you might choose to have more elaborate message and adaptive cards. My only suggestion would be to do this while the weather holds and fingers are still nimble. Of course, Microsoft may decide to revamp everything again and render this script totally useless, but I honestly cannot plan for that.

— Dr Saurabh Sawhney

This writeup is best read as a companion piece to Alertmanager to MS Teams via Power Automate — the scenic route

--

--

Responses (1)