Skip to content

Instantly share code, notes, and snippets.

@chusiang
Last active October 28, 2025 15:53
Show Gist options
  • Save chusiang/895f6406fbf9285c58ad0a3ace13d025 to your computer and use it in GitHub Desktop.
Save chusiang/895f6406fbf9285c58ad0a3ace13d025 to your computer and use it in GitHub Desktop.
Post a message to Microsoft Teams with bash script.
#!/bin/bash
# =============================================================================
# Author: Chu-Siang Lai / chusiang (at) drx.tw
# Filename: teams-chat-post-for-workflows.sh
# Modified: 2024-07-22 11:44 (UTC+08:00)
# Description: Post a message to Microsoft Teams via "Post to a chat when a webhook request is received" workflows.
# Reference:
#
# - https://gist.github.com/chusiang/895f6406fbf9285c58ad0a3ace13d025
# - https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/
# - https://adaptivecards.io/explorer/
# - https://adaptivecards.io/designer/
#
# =============================================================================
# Help.
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
echo 'Usage: teams-chat-post.sh "<webhook_url>" "<title>" "<color>" "<message>"'
exit 0
fi
# Webhook or Token.
WEBHOOK_URL=$1
if [[ "${WEBHOOK_URL}" == "" ]]; then
echo "No webhook_url specified."
exit 1
fi
shift
# Title .
TITLE=$1
if [[ "${TITLE}" == "" ]]; then
echo "No title specified."
exit 1
fi
shift
# Color.
COLOR=$1
if [[ "${COLOR}" == "" ]]; then
echo "No color specified."
exit 1
fi
shift
# Text.
TEXT=$*
if [[ "${TEXT}" == "" ]]; then
echo "No text specified."
exit 1
fi
# Escape char: `'`, `"`, `\` .
MESSAGE=$(echo ${TEXT} | sed "s/'/\'/g" | sed 's/"/\"/g; s/\\/\\\\/g')
# Adaptive Cards of TextBlock - https://adaptivecards.io/explorer/TextBlock.html
JSON="{
\"type\": \"message\",
\"attachments\": [
{
\"contentType\": \"application/vnd.microsoft.card.adaptive\",
\"contentUrl\": null,
\"content\": {
\"$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",
\"type\": \"AdaptiveCard\",
\"version\": \"1.2\",
\"body\": [
{
\"type\": \"TextBlock\",
\"text\": \"${TITLE}\",
\"color\": \"${COLOR}\",
\"weight\": \"bolder\",
\"size\": \"large\",
\"wrap\": true
},
{
\"type\": \"TextBlock\",
\"text\": \"${MESSAGE}\",
\"color\": \"${COLOR}\",
\"size\": \"small\",
\"wrap\": true
}
]
}
}
]
}"
# Post to Microsoft Teams via curl.
curl \
--header "Content-Type: application/json" \
--request POST \
--data "${JSON}" \
"${WEBHOOK_URL}"
#!/bin/bash
# =============================================================================
# Author: Chu-Siang Lai / chusiang (at) drx.tw
# Filename: teams-chat-post.sh
# Modified: 2024-07-12 18:49 (UTC+08:00)
# Description: Post a message to Microsoft Teams via connectors, not support
# Power Automate workflows.
# Reference:
#
# - https://gist.github.com/chusiang/895f6406fbf9285c58ad0a3ace13d025
# - https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/
# - Fixed for workflows edition: https://gist.github.com/chusiang/895f6406fbf9285c58ad0a3ace13d025?permalink_comment_id=5119162#gistcomment-5119162
#
# =============================================================================
# Help.
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
echo 'Usage: teams-chat-post.sh "<webhook_url>" "<title>" "<color>" "<message>"'
exit 0
fi
# Webhook or Token.
WEBHOOK_URL=$1
if [[ "${WEBHOOK_URL}" == "" ]]; then
echo "No webhook_url specified."
exit 1
fi
shift
# Title .
TITLE=$1
if [[ "${TITLE}" == "" ]]; then
echo "No title specified."
exit 1
fi
shift
# Color.
COLOR=$1
if [[ "${COLOR}" == "" ]]; then
echo "No status specified."
exit 1
fi
shift
# Text.
TEXT=$*
if [[ "${TEXT}" == "" ]]; then
echo "No text specified."
exit 1
fi
# Convert formating.
MESSAGE=$(echo ${TEXT} | sed 's/"/\"/g' | sed "s/'/\'/g")
JSON="{
\"title\": \"${TITLE}\",
\"themeColor\": \"${COLOR}\",
\"text\": \"${MESSAGE}\"
}"
# Post to Microsoft Teams.
curl -H "Content-Type: application/json" -d "${JSON}" "${WEBHOOK_URL}"
@emibcn
Copy link

emibcn commented May 5, 2025

Example:

image

@yondkoo
Copy link

yondkoo commented May 8, 2025

I have done a rewrite of the script:

  • Use jq for managing JSON
  • Use getopts
  • Added a bunch of optional parameters
#!/bin/bash
# =============================================================================
#  Original Author: Chu-Siang Lai / chusiang (at) drx.tw
#  Modifier: @emibcn https://github.com/emibcn
#  Filename: teams-chat-post-for-workflows.sh
#  Description: Post a message to Microsoft Teams via "Post to a chat when a webhook request is received" workflows.
#  Reference:
#
#   - https://gist.github.com/chusiang/895f6406fbf9285c58ad0a3ace13d025
#   - https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/
#   - https://adaptivecards.io/explorer/
#   - https://adaptivecards.io/designer/
#
# =============================================================================

set -Eeuo pipefail

usage() {
    cat <<EOF
Usage:
  teams-chat-post.sh -w "<webhook_url>" -t "<title>" -x "<message>"

Optional parameters:
  -h                        Get usage help (this text)
  -c "<Color>"              (default: "default")
  -u "<Button URL>"         If not used, no button is shown
  -b "<Button Text>"        (default: "URL")
  -i "<IMAGE_URL>"          If not used, no image is shown
  -p "<Expandable title>"   Used for the expand button (Default: "View more")
  -e "<Expandable text>"    If not used, no expandable text is shown
  -l "<Expandable logo>"    Used as logoo for the "View more" button
EOF
    exit ${1:-0}
}

# Get options into variables
while getopts :hw:c:t:x:u:b:i:p:e:l: OPTION
do
    case ${OPTION} in
        w) WEBHOOK_URL="${OPTARG}" ;;
        t) TITLE="${OPTARG}" ;;
        x) TEXT="${OPTARG}" ;;
        c) COLOR="${OPTARG}" ;;
        u) BUTTON_URL="${OPTARG}" ;;
        b) BUTTON_TEXT="${OPTARG}" ;;
        i) IMAGE_URL="${OPTARG}" ;;
        p) EXPANDABLE_TITLE="${OPTARG}" ;;
        e) EXPANDABLE_TEXT="${OPTARG}" ;;
        l) EXPANDABLE_LOGO="${OPTARG}" ;;
        h)
          usage
          ;;
        :)
          echo "${0}: ERROR: Must supply an argument to -${OPTARG}" >&2
          usage 2
          ;;
        ?)
          echo "${0}: ERROR: Invalid option: -${OPTARG}" >&2
          usage 3
          ;;

    esac
done

#
# Verify variables
#

# Webhook or Token
if [ "${WEBHOOK_URL:-}" == "" ]
then
    echo "No webhook_url specified."
    echo "Visit this URL to see how to obtain one:"
    echo "https://support.microsoft.com/en-us/office/creating-a-workflow-from-a-chat-in-teams-e3b51c4f-49de-40aa-a6e7-bcff96b99edc"
    usage 1
fi

# Color
# https://adaptivecards.io/explorer/TextBlock.html
ALLOWED_COLORS=(
    default
    dark
    light
    accent
    good
    warning
    attention
)
COLOR="${COLOR:-default}"
if [ "${COLOR:-}" == "" ]
then
    echo "No color specified."
    usage 1
fi
if ! printf '%s\0' "${ALLOWED_COLORS[@]}" | grep -qwz "${COLOR}"
then
    echo "The specified color is not in the allowed list:"
    echo "$(IFS="," ; echo "${ALLOWED_COLORS[*]}")"
    usage 1
fi

# Title
if [ "${TITLE:-}" == "" ]
then
    echo "No title specified."
    usage 1
fi

# Text
if [ "${TEXT:-}" == "" ]
then
    echo "No text specified."
    usage 1
fi

# Generate JSON using variables
# Adaptive Cards of TextBlock - https://adaptivecards.io/explorer/TextBlock.html
JSON="$(
    jq \
        --null-input \
        --arg COLOR "${COLOR}" \
        --arg TITLE "${TITLE}" \
        --arg TEXT "${TEXT}" \
        '{
          type: "message",
          attachments: [
            {
              contentType: "application/vnd.microsoft.card.adaptive",
              contentUrl: null,
              content: {
                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                type: "AdaptiveCard",
                version: "1.2",
                actions: [],
                body: [
                  {
                    type: "TextBlock",
                    text: $TITLE,
                    color:  $COLOR,
                    eight: "bolder",
                    size: "large",
                    wrap: true
                  },
                  {
                    type: "TextBlock",
                    text: $TEXT,
                    color: $COLOR,
                    size: "small",
                    wrap: true
                  }
                ]
              }
            }
          ]
        }'
)"

if [ "${BUTTON_URL:-}" != "" ]
then
    JSON="$(
        echo "${JSON}" \
            | jq \
                --arg URL "${BUTTON_URL}" \
                --arg TEXT "${BUTTON_TEXT:-URL}" \
                '.attachments[0].content.actions += [
                  {
                    type: "Action.OpenUrl",
                    title: $TEXT,
                    url: $URL
                  }
                ]
                '
    )"
fi

if [ "${EXPANDABLE_TEXT:-}" != "" ]
then
    JSON="$(
        echo "${JSON}" \
            | jq \
                --arg TITLE "${EXPANDABLE_TITLE:-View more}" \
                --arg TEXT "${EXPANDABLE_TEXT}" \
                --arg URL "${EXPANDABLE_LOGO:-}" \
                '.attachments[0].content.actions += [
                    {
                      "type": "Action.ShowCard",
                      "title": $TITLE,
                      iconUrl: $URL,
                      "card": {
                        "type": "AdaptiveCard",
                        "body": [
                          {
                            type: "TextBlock",
                            text: $TEXT,
                            size: "small",
                            wrap: true
                          }
                        ]
                      }
                    }
                  ]
                '
    )"
fi

if [ "${IMAGE_URL:-}" != "" ]
then
    JSON="$(
        echo "${JSON}" \
            | jq \
                --arg IMAGE "${IMAGE_URL}" \
                '.attachments[0].content.body = [
                    {
                      type: "ColumnSet",
                      columns: [
                        {
                          type: "Column",
                          items: .attachments[0].content.body
                        },
                        {
                          type: "Column",
                          width: "auto",
                          verticalContentAlignment: "center",
                          items: [
                            {
                              type: "Image",
                              url: $IMAGE
                            }
                          ]
                        }
                      ]
                    }
                  ]
                '
    )"
fi

# Post to Microsoft Teams using curl
curl \
    -v \
    --header "Content-Type: application/json" \
    --request POST \
    --data "${JSON}" \
    "${WEBHOOK_URL}"

-eight, +weight

@emibcn
Copy link

emibcn commented May 8, 2025

@yondkoo Thanks for spotting it out. Fixed!

Also fixed:

  • -logoo, +logo

@rbrtzmck
Copy link

rbrtzmck commented Oct 8, 2025

Hi,

I can use the script (again) but I am unable to show the JSON content directly as a card. I receive runtime errors.

I have this Workflow.

image

and I send the data like this (the webhook is written inside the script already)

./teams-workflow.sh -t "TITLE" -x "MESSAGE"

The left branch works - I get a static notification. However, I do not know how to setup properly the "post card" step.

image

The data are visible in the Workflow, I can see my "TITLE" and "MESSAGE" texts in the received Adaptive card.

Do you know please how to setup this minimal example? Or what kind of Workflow do you use to show the sh-script-generated JSON payload? Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment