Skip to content

Instantly share code, notes, and snippets.

@chusiang
Last active May 8, 2025 13:44
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}"
@alaxelmck
Copy link

alaxelmck commented Sep 18, 2024

Hi @lp-maximus,

I think we need add Escape Character for \ on L60 in teams-chat-post-for-workflows.sh.

https://gist.github.com/chusiang/895f6406fbf9285c58ad0a3ace13d025#file-teams-chat-post-for-workflows-sh-L60

I have also escape the \ char, maybe it can help you.

- MESSAGE=$(echo ${TEXT} | sed 's/"/\"/g' | sed "s/'/\'/g")
+ MESSAGE=$(echo ${TEXT} | sed "s/'/\'/g" | sed 's/"/\"/g; s/\\/\\\\/g')

Good luck.

Reference:

  1. Which characters need to be escaped when using Bash? | Stack Overflow
  2. How to use sed to find and replace text in files in Linux / Unix shell - nixCraft

Hi,

In my case I'm passing to the script an output of another script, which takes sql query result and concats with another line having an escaped newline:
query-db-and-pass-to-teams-script.sh
MESSAGE="$SQL_RESULT" # Which is a 5 line string, 1st 4 ending with literal newlines.
MESSAGE+="\nAnother line here"
${TEAMS_SCRIPT} "$WEBHOOK_URL" "$TITLE" "$COLOR" "$MESSAGE"

So when this $MESSAGE is passed to your script, several issues occur at this line:
MESSAGE=$(echo ${TEXT} | sed "s/'/\'/g" | sed 's/"/\"/g; s/\\/\\\\/g')

  1. If we use echo ${TEXT} - it will eat all literal newlines. Had to use echo "${TEXT}" to avoid that.
  2. s/\/\\/g turns my concated escaped newline in "\nAnother line here" into "\\\\nAnother line here"

P.S.
echo "No status specified."
Was it meant "No color specified."?

@chusiang
Copy link
Author

chusiang commented Sep 19, 2024

Hi @alaxelmck,

I have update to No color specified., it's typo with I copy from my old script like slack-chat-post.sh .

About support output of another script part, I have no idea now.

@emibcn
Copy link

emibcn commented May 5, 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 logo 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,
                    weight: "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}"

@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

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