├── .gitignore ├── README.md ├── docker-compose.yaml ├── example.png ├── ms_teams_powerautomate_webhook_operator.py ├── sample_dag.py ├── samplecard.json └── screenshots ├── 001.png ├── 002.png ├── 003.png ├── 004.png ├── 005.png └── 006.png /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | logs/ 3 | plugins/ 4 | config/ 5 | dags/* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This is an Airflow operator that can send cards to MS Teams via webhooks. There are various options to customize the appearance of the cards. 3 | 4 | ## Screenshots 5 | 6 | 7 | 8 | | Header, subtitle, and body | Header, subtitle, body, facts, and a button | 9 | | ---------------------------------------------------------------- | -------------------------------------------------------------------------- | 10 | | ![Card with a header, subtitle, and body](./screenshots/001.png) | ![Card with a header, subtitle, body, and a button](./screenshots/004.png) | 11 | 12 | | Body with coloured text and coloured button | Coloured header, body, button, in dark mode | 13 | | --------------------------------------------------------------------- | ------------------------------------------------------------ | 14 | | ![Body with coloured text and coloured button](./screenshots/002.png) | ![Header, body, button, in dark mode](./screenshots/003.png) | 15 | 16 | | Body and empty green header | Body and coloured header, without logo | 17 | | ------------------------------------------------------ | ------------------------------------------------------- | 18 | | ![Body with empty green header](./screenshots/005.png) | ![Body and header, without logo](./screenshots/006.png) | 19 | 20 | 21 | ## Setup 22 | 23 | Create a webhook to post to Teams. The Webhook needs to be of the PowerAutomate type, not the deprecated Incoming Webhook type. Currently this is done either through the 'workflows' app in Teams, or via [PowerAutomate](https://powerautomate.com). 24 | 25 | Once that's ready, [create an HTTP Connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) in Airflow with the Webhook URL. 26 | 27 | * Conn Type: HTTP 28 | * Host: The URL without the https:// 29 | * Schema: https 30 | 31 | Copy the [ms_teams_power_automate_webhook_operator.py](./ms_teams_powerautomate_webhook_operator.py) file into your Airflow dags folder and `import` it in your DAG code. 32 | 33 | ## Usage 34 | 35 | The usage can be very basic from just a message, to [several parameters](#parameters) including a full card with header, subtitle, body, facts, and a button. There are some style options too. 36 | 37 | A very basic message: 38 | 39 | ```python 40 | op1 = MSTeamsPowerAutomateWebhookOperator( 41 | task_id="send_to_teams", 42 | http_conn_id="msteams_webhook_url", 43 | body_message="DAG **lorem_ipsum** has completed successfully in **localhost**", 44 | ) 45 | ``` 46 | 47 | Add a button: 48 | 49 | ```python 50 | op1 = MSTeamsPowerAutomateWebhookOperator( 51 | task_id="send_to_teams", 52 | http_conn_id="msteams_webhook_url", 53 | body_message="DAG **lorem_ipsum** has completed successfully in **localhost**", 54 | button_text="View Logs", 55 | button_url="https://example.com", 56 | ) 57 | ``` 58 | 59 | Add a heading and subtitle: 60 | 61 | ```python 62 | op1 = MSTeamsPowerAutomateWebhookOperator( 63 | task_id="send_to_teams", 64 | http_conn_id="msteams_webhook_url", 65 | heading_title="DAG **lorem_ipsum** has completed successfully", 66 | heading_subtitle="In **localhost**", 67 | body_message="DAG **lorem_ipsum** has completed successfully in **localhost**", 68 | button_text="View Logs", 69 | button_url="https://example.com", 70 | ) 71 | ``` 72 | 73 | Add some colouring — header bar colour, subtle subtitle, body text colour, button colour: 74 | 75 | ```python 76 | op1 = MSTeamsPowerAutomateWebhookOperator( 77 | task_id="send_to_teams", 78 | http_conn_id="msteams_webhook_url", 79 | header_bar_style="good", 80 | heading_title="DAG **lorem_ipsum** has completed successfully", 81 | heading_subtitle="In **localhost**", 82 | heading_subtitle_subtle=False, 83 | body_message="DAG **lorem_ipsum** has completed successfully in **localhost**", 84 | body_message_color_type="good", 85 | button_text="View Logs", 86 | button_url="https://example.com", 87 | button_style="positive", 88 | ) 89 | ``` 90 | 91 | You can also look at [sample_dag.py](./sample_dag.py) for an example of how to use this operator in a DAG. 92 | 93 | 94 | ## Parameters 95 | 96 | Here are all the parameters that can be set. 97 | 98 | | Parameter | Values | Notes | 99 | | ----------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | 100 | | http_conn_id | The connection ID, eg "msteams_webhook_url" | | 101 | | card_width_full | True(default) or False | If false, the card will be the MSTeams default. | 102 | | header_bar_show | True(default) or False | If false, heading title, subtitle, logo won't be shown. | 103 | | header_bar_style | `default`, `emphasis`, `good`, `attention`, `warning`, `accent` | [docs - style](https://adaptivecards.io/explorer/Container.html) | 104 | | heading_title | | [Limited Markdown support](https://aka.ms/ACTextFeatures), no `monospace` | 105 | | heading_title_size | `default`, `small`, `medium`, `large`, `extraLarge` | [docs - size](https://adaptivecards.io/explorer/TextBlock.html) | 106 | | heading_subtitle | | Appears just below the title, [Limited Markdown support](https://aka.ms/ACTextFeatures), no `monospace` | 107 | | heading_subtitle_subtle | True(default) or False | Subtle means toned down to appear less prominent | 108 | | heading_show_logo | True(default) or False | | 109 | | body_message | | [Limited Markdown support](https://aka.ms/ACTextFeatures), no `monospace` | 110 | | body_message_color_type | `default`, `dark`, `light`, `accent`, `good`, `warning`, `attention` | [docs - color](https://adaptivecards.io/explorer/TextBlock.html) | 111 | | body_facts_dict | Example: {'aaa':'bbb','ccc':'ddd'} | The key value pairs show up as facts in the card | 112 | | button_text | Example: "View Logs" | If not set, button won't be shown | 113 | | button_url | Example: "https://example.com" | For example, the URL to the Airflow log | 114 | | button_style | `default`, `positive`, `destructive` | [docs - style](https://adaptivecards.io/explorer/Action.OpenUrl.html) | 115 | | button_show | True(default) or False | | 116 | 117 | 118 | 119 | 120 | ## The old incoming webhooks 121 | 122 | This operator only works with the new PowerAutomate webhooks. The old incoming webhooks were deprecated in this [Teams announcement](https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/). It says they'll keep working until December 2025 but I expect much degradation in the service before then. 123 | 124 | The previous version of this operator, which worked with the old incoming webhooks, is in [the master-old-connectors branch](https://github.com/mendhak/Airflow-MS-Teams-Operator/tree/master-old-connectors). This operator is not a drop-in replacement for the old one, as there are too many differences. 125 | 126 | ## Contribute 127 | 128 | Any simple feature requests, please fork and submit a PR. 129 | 130 | ## Testing this operator locally for development 131 | 132 | I'm using the [Airflow Docker Compose](https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html), with a few changes. Load examples is false and added an extra_hosts. 133 | 134 | Run this to prepare the environment: 135 | 136 | ``` 137 | mkdir -p ./dags ./logs ./plugins ./config 138 | echo -e "AIRFLOW_UID=$(id -u)" > .env 139 | docker compose up airflow-init 140 | docker compose up 141 | ``` 142 | 143 | Then wait a bit, and open http://localhost:8080 with airflow:airflow. 144 | 145 | To create a connection quickly, use this CLI command, substitute the url bit. 146 | 147 | ``` 148 | docker compose exec -it airflow-webserver airflow connections add 'msteams_webhook_url' --conn-json '{"conn_type": "http", "description": "", "host": "", "schema": "https", "login": "", "password": null, "port": null }' 149 | ``` 150 | 151 | Now run the sample_dag to see the operator in action. 152 | 153 | ### Echoing requests 154 | 155 | To troubleshoot the requests going out, use the included httpecho container which echoes the request to output. 156 | In Airflow connections, create an HTTP Connection to http://httpecho:8081 157 | 158 | 159 | ``` 160 | docker compose exec -it airflow-webserver airflow connections add 'msteams_webhook_url' --conn-json '{"conn_type": "http", "description": "", "host": "httpecho:8081/a/b/c", "schema": "http", "login": "", "password": null, "port": null }' 161 | 162 | docker compose logs -f httpecho 163 | ``` 164 | 165 | Now when you run the sample_dag, you can see the request reflected in the httpecho container. 166 | 167 | ### Posting a card with curl 168 | 169 | To manually post the sample card to a webhook URL, just for testing, use the included samplecard.json file. 170 | 171 | ``` 172 | curl -X POST -H 'Content-Type: application/json' --data-binary @samplecard.json "https://prod-11.westus.logic.azure.com:443/workflows/.............." 173 | ``` 174 | 175 | 176 | ## License 177 | 178 | Apache 2.0 (see code file headers) -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | 19 | # Basic Airflow cluster configuration for CeleryExecutor with Redis and PostgreSQL. 20 | # 21 | # WARNING: This configuration is for local development. Do not use it in a production deployment. 22 | # 23 | # This configuration supports basic configuration using environment variables or an .env file 24 | # The following variables are supported: 25 | # 26 | # AIRFLOW_IMAGE_NAME - Docker image name used to run Airflow. 27 | # Default: apache/airflow:2.9.3 28 | # AIRFLOW_UID - User ID in Airflow containers 29 | # Default: 50000 30 | # AIRFLOW_PROJ_DIR - Base path to which all the files will be volumed. 31 | # Default: . 32 | # Those configurations are useful mostly in case of standalone testing/running Airflow in test/try-out mode 33 | # 34 | # _AIRFLOW_WWW_USER_USERNAME - Username for the administrator account (if requested). 35 | # Default: airflow 36 | # _AIRFLOW_WWW_USER_PASSWORD - Password for the administrator account (if requested). 37 | # Default: airflow 38 | # _PIP_ADDITIONAL_REQUIREMENTS - Additional PIP requirements to add when starting all containers. 39 | # Use this option ONLY for quick checks. Installing requirements at container 40 | # startup is done EVERY TIME the service is started. 41 | # A better way is to build a custom image or extend the official image 42 | # as described in https://airflow.apache.org/docs/docker-stack/build.html. 43 | # Default: '' 44 | # 45 | # Feel free to modify this file to suit your needs. 46 | --- 47 | x-airflow-common: 48 | &airflow-common 49 | # In order to add custom dependencies or upgrade provider packages you can use your extended image. 50 | # Comment the image line, place your Dockerfile in the directory where you placed the docker-compose.yaml 51 | # and uncomment the "build" line below, Then run `docker-compose build` to build the images. 52 | image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:2.9.3} 53 | # build: . 54 | extra_hosts: 55 | - "host.docker.internal:host-gateway" 56 | environment: 57 | &airflow-common-env 58 | AIRFLOW__CORE__EXECUTOR: LocalExecutor 59 | AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow 60 | AIRFLOW__CORE__FERNET_KEY: '' 61 | AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true' 62 | AIRFLOW__CORE__LOAD_EXAMPLES: 'false' 63 | 64 | AIRFLOW__API__AUTH_BACKENDS: 'airflow.api.auth.backend.basic_auth,airflow.api.auth.backend.session' 65 | # yamllint disable rule:line-length 66 | # Use simple http server on scheduler for health checks 67 | # See https://airflow.apache.org/docs/apache-airflow/stable/administration-and-deployment/logging-monitoring/check-health.html#scheduler-health-check-server 68 | # yamllint enable rule:line-length 69 | AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK: 'true' 70 | # WARNING: Use _PIP_ADDITIONAL_REQUIREMENTS option ONLY for a quick checks 71 | # for other purpose (development, test and especially production usage) build/extend Airflow image. 72 | _PIP_ADDITIONAL_REQUIREMENTS: ${_PIP_ADDITIONAL_REQUIREMENTS:-} 73 | # The following line can be used to set a custom config file, stored in the local config folder 74 | # If you want to use it, outcomment it and replace airflow.cfg with the name of your config file 75 | # AIRFLOW_CONFIG: '/opt/airflow/config/airflow.cfg' 76 | volumes: 77 | - ${AIRFLOW_PROJ_DIR:-.}/dags:/opt/airflow/dags 78 | - ${AIRFLOW_PROJ_DIR:-.}/ms_teams_powerautomate_webhook_operator.py:/opt/airflow/dags/ms_teams_powerautomate_webhook_operator.py 79 | - ${AIRFLOW_PROJ_DIR:-.}/sample_dag.py:/opt/airflow/dags/sample_dag.py 80 | - ${AIRFLOW_PROJ_DIR:-.}/logs:/opt/airflow/logs 81 | - ${AIRFLOW_PROJ_DIR:-.}/config:/opt/airflow/config 82 | - ${AIRFLOW_PROJ_DIR:-.}/plugins:/opt/airflow/plugins 83 | user: "${AIRFLOW_UID:-50000}:0" 84 | depends_on: 85 | &airflow-common-depends-on 86 | postgres: 87 | condition: service_healthy 88 | 89 | services: 90 | postgres: 91 | image: postgres:13 92 | environment: 93 | POSTGRES_USER: airflow 94 | POSTGRES_PASSWORD: airflow 95 | POSTGRES_DB: airflow 96 | volumes: 97 | - postgres-db-volume:/var/lib/postgresql/data 98 | healthcheck: 99 | test: ["CMD", "pg_isready", "-U", "airflow"] 100 | interval: 10s 101 | retries: 5 102 | start_period: 5s 103 | restart: always 104 | 105 | 106 | airflow-webserver: 107 | <<: *airflow-common 108 | command: webserver 109 | ports: 110 | - "8080:8080" 111 | healthcheck: 112 | test: ["CMD", "curl", "--fail", "http://localhost:8080/health"] 113 | interval: 30s 114 | timeout: 10s 115 | retries: 5 116 | start_period: 30s 117 | restart: always 118 | depends_on: 119 | <<: *airflow-common-depends-on 120 | airflow-init: 121 | condition: service_completed_successfully 122 | 123 | airflow-scheduler: 124 | <<: *airflow-common 125 | command: scheduler 126 | healthcheck: 127 | test: ["CMD", "curl", "--fail", "http://localhost:8974/health"] 128 | interval: 30s 129 | timeout: 10s 130 | retries: 5 131 | start_period: 30s 132 | restart: always 133 | depends_on: 134 | <<: *airflow-common-depends-on 135 | airflow-init: 136 | condition: service_completed_successfully 137 | 138 | airflow-init: 139 | <<: *airflow-common 140 | entrypoint: /bin/bash 141 | # yamllint disable rule:line-length 142 | command: 143 | - -c 144 | - | 145 | if [[ -z "${AIRFLOW_UID}" ]]; then 146 | echo 147 | echo -e "\033[1;33mWARNING!!!: AIRFLOW_UID not set!\e[0m" 148 | echo "If you are on Linux, you SHOULD follow the instructions below to set " 149 | echo "AIRFLOW_UID environment variable, otherwise files will be owned by root." 150 | echo "For other operating systems you can get rid of the warning with manually created .env file:" 151 | echo " See: https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#setting-the-right-airflow-user" 152 | echo 153 | fi 154 | one_meg=1048576 155 | mem_available=$$(($$(getconf _PHYS_PAGES) * $$(getconf PAGE_SIZE) / one_meg)) 156 | cpus_available=$$(grep -cE 'cpu[0-9]+' /proc/stat) 157 | disk_available=$$(df / | tail -1 | awk '{print $$4}') 158 | warning_resources="false" 159 | if (( mem_available < 4000 )) ; then 160 | echo 161 | echo -e "\033[1;33mWARNING!!!: Not enough memory available for Docker.\e[0m" 162 | echo "At least 4GB of memory required. You have $$(numfmt --to iec $$((mem_available * one_meg)))" 163 | echo 164 | warning_resources="true" 165 | fi 166 | if (( cpus_available < 2 )); then 167 | echo 168 | echo -e "\033[1;33mWARNING!!!: Not enough CPUS available for Docker.\e[0m" 169 | echo "At least 2 CPUs recommended. You have $${cpus_available}" 170 | echo 171 | warning_resources="true" 172 | fi 173 | if (( disk_available < one_meg * 10 )); then 174 | echo 175 | echo -e "\033[1;33mWARNING!!!: Not enough Disk space available for Docker.\e[0m" 176 | echo "At least 10 GBs recommended. You have $$(numfmt --to iec $$((disk_available * 1024 )))" 177 | echo 178 | warning_resources="true" 179 | fi 180 | if [[ $${warning_resources} == "true" ]]; then 181 | echo 182 | echo -e "\033[1;33mWARNING!!!: You have not enough resources to run Airflow (see above)!\e[0m" 183 | echo "Please follow the instructions to increase amount of resources available:" 184 | echo " https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#before-you-begin" 185 | echo 186 | fi 187 | mkdir -p /sources/logs /sources/dags /sources/plugins 188 | chown -R "${AIRFLOW_UID}:0" /sources/{logs,dags,plugins} 189 | exec /entrypoint airflow version 190 | # yamllint enable rule:line-length 191 | environment: 192 | <<: *airflow-common-env 193 | _AIRFLOW_DB_MIGRATE: 'true' 194 | _AIRFLOW_WWW_USER_CREATE: 'true' 195 | _AIRFLOW_WWW_USER_USERNAME: ${_AIRFLOW_WWW_USER_USERNAME:-airflow} 196 | _AIRFLOW_WWW_USER_PASSWORD: ${_AIRFLOW_WWW_USER_PASSWORD:-airflow} 197 | _PIP_ADDITIONAL_REQUIREMENTS: '' 198 | user: "0:0" 199 | volumes: 200 | - ${AIRFLOW_PROJ_DIR:-.}:/sources 201 | 202 | httpecho: 203 | image: mendhak/http-https-echo:33 204 | environment: 205 | - HTTP_PORT=8081 206 | - HTTPS_PORT=9999 207 | ports: 208 | - "8081:8081" 209 | - "8443:9999" 210 | 211 | volumes: 212 | postgres-db-volume: 213 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendhak/Airflow-MS-Teams-Operator/3361c32cd069976793d640343037943551595ec8/example.png -------------------------------------------------------------------------------- /ms_teams_powerautomate_webhook_operator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | from airflow.providers.http.hooks.http import HttpHook 21 | from airflow.providers.http.operators.http import HttpOperator 22 | from airflow.utils.decorators import apply_defaults 23 | import logging 24 | import json 25 | 26 | 27 | class MSTeamsPowerAutomateWebhookOperator(HttpOperator): 28 | """ 29 | This operator allows you to post messages to MS Teams using the Incoming Webhooks connector. 30 | Takes both MS Teams webhook token directly and connection that has MS Teams webhook token. 31 | If both supplied, the webhook token will be appended to the host in the connection. 32 | 33 | :param http_conn_id: connection that has MS Teams webhook URL 34 | :type http_conn_id: str 35 | 36 | :param card_width_full: Whether to show the card in full width. If false, the card will be the MSTeams default 37 | :type card_width_full: bool 38 | :param header_bar_show: Whether to show the header in the card. If false, heading title, subtitle, logo won't be shown. 39 | :type header_bar_show: bool 40 | :param header_bar_style: The style hint for the header bar: `default`, `emphasis`, `good`, `attention`, `warning`, `accent`. 41 | :type header_bar_style: str 42 | :param heading_title: The title of the card 43 | :type heading_title: str 44 | :param heading_title_size: The size of the heading_title: `default`, `small`, `medium`, `large`, `extraLarge`. 45 | :param heading_subtitle: The subtitle of the card, just below the heading_title 46 | :type heading_subtitle: str 47 | :param heading_subtitle_subtle: Whether the subtitle should be subtle (toned down to appear less prominent) 48 | :param heading_show_logo: Whether to show the Airflow logo in the card 49 | :type heading_show_logo: bool 50 | :param body_message: The main message of the card 51 | :type body_message: str 52 | :param body_message_color_type: The color 'type' of the body message: `default`, `dark`, `light`, `accent`, `good`, `warning`, `attention`. 53 | :param body_facts_dict: The dictionary of facts to show in the card 54 | :type body_facts_dict: dict 55 | :param button_text: The text of the action button 56 | :type button_text: str 57 | :param button_url: The URL for the action button click 58 | :type button_url: str 59 | :param button_style: The action style of the button: `default`, `positive`, `destructive` 60 | :param button_show: Whether to show the action button 61 | :type button_show: bool 62 | """ 63 | 64 | template_fields = ("heading_title", "heading_subtitle", "body_message") 65 | 66 | @apply_defaults 67 | def __init__( 68 | self, 69 | http_conn_id=None, 70 | card_width_full=True, 71 | header_bar_show=True, 72 | header_bar_style="default", 73 | heading_title=None, 74 | heading_title_size="large", 75 | heading_subtitle=None, 76 | heading_subtitle_subtle=True, 77 | heading_show_logo=True, 78 | body_message="", 79 | body_message_color_type="default", 80 | body_facts_dict=None, 81 | button_text=None, 82 | button_url="https://example.com", 83 | button_style="default", 84 | button_show=True, 85 | *args, 86 | **kwargs 87 | ): 88 | 89 | super(MSTeamsPowerAutomateWebhookOperator, self).__init__(*args, **kwargs) 90 | self.http_conn_id = http_conn_id 91 | 92 | self.card_width_full = card_width_full 93 | 94 | self.header_bar_show = header_bar_show 95 | self.header_bar_style = header_bar_style 96 | self.heading_title = heading_title 97 | self.heading_title_size = heading_title_size 98 | self.heading_subtitle = heading_subtitle 99 | self.heading_subtitle_subtle = heading_subtitle_subtle 100 | self.heading_show_logo = heading_show_logo 101 | 102 | self.body_message = body_message 103 | self.body_message_color_type = body_message_color_type 104 | self.body_facts_dict = body_facts_dict 105 | 106 | self.button_text = button_text 107 | self.button_url = button_url 108 | self.button_show = button_show 109 | self.button_style = button_style 110 | 111 | def build_message(self): 112 | cardjson = { 113 | "type": "message", 114 | "attachments": [ 115 | { 116 | "contentType": "application/vnd.microsoft.card.adaptive", 117 | "contentUrl": None, 118 | "content": { 119 | "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", 120 | "type": "AdaptiveCard", 121 | "version": "1.0", 122 | "body": [ 123 | { 124 | "type": "Container", 125 | "isVisible": self.header_bar_show, 126 | "style": self.header_bar_style, 127 | "bleed": True, 128 | "minHeight": "15px", 129 | "spacing": "None", 130 | "items": [ 131 | { 132 | "type": "ColumnSet", 133 | "columns": [ 134 | { 135 | "type": "Column", 136 | "width": "auto", 137 | "items": [ 138 | { 139 | "type": "Image", 140 | "url": "data:image/png;base64,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", 141 | "altText": "Airflow logo", 142 | "size": "small", 143 | "style": "default", 144 | "isVisible": self.heading_show_logo, 145 | } 146 | ], 147 | }, 148 | { 149 | "type": "Column", 150 | "width": "stretch", 151 | "items": [ 152 | { 153 | "type": "TextBlock", 154 | "text": self.heading_title, 155 | "weight": "bolder", 156 | "size": self.heading_title_size, 157 | "wrap": True, 158 | "style": "heading", 159 | "isVisible": self.heading_title is not None 160 | }, 161 | { 162 | "type": "TextBlock", 163 | "spacing": "none", 164 | "text": self.heading_subtitle, 165 | "isSubtle": self.heading_subtitle_subtle, 166 | "wrap": True, 167 | "isVisible": self.heading_subtitle is not None 168 | }, 169 | ], 170 | }, 171 | ], 172 | } 173 | ], 174 | }, 175 | { 176 | "type": "Container", 177 | "items": [ 178 | { 179 | "type": "TextBlock", 180 | "text": self.body_message, 181 | "wrap": True, 182 | "color": self.body_message_color_type, 183 | }, 184 | { 185 | "type": "FactSet", 186 | "isVisible": True, 187 | "facts": [ 188 | 189 | ] 190 | } 191 | ], 192 | }, 193 | ], 194 | "actions": [ 195 | { 196 | "type": "Action.OpenUrl", 197 | "title": self.button_text, 198 | "url": self.button_url, 199 | "style": self.button_style, 200 | } 201 | ], 202 | }, 203 | } 204 | ], 205 | } 206 | 207 | if self.body_facts_dict and len(self.body_facts_dict.items()) > 0: 208 | logging.info("Adding facts to the card") 209 | cardjson["attachments"][0]["content"]["body"][1]["items"][1]["facts"] = [] 210 | for key, value in self.body_facts_dict.items(): 211 | cardjson["attachments"][0]["content"]["body"][1]["items"][1]["facts"].append({"title": key, "value": value}) 212 | 213 | 214 | if self.card_width_full: 215 | cardjson["attachments"][0]["content"]["msteams"] = {"width": "Full"} 216 | 217 | if not self.button_show or not self.button_text: 218 | del cardjson["attachments"][0]["content"]["actions"][0] 219 | 220 | 221 | return json.dumps(cardjson) 222 | 223 | def execute(self, context): 224 | """ 225 | Call the http hook and just invoke it. 226 | """ 227 | 228 | card_json = self.build_message() 229 | http = HttpHook(http_conn_id=self.http_conn_id, method="POST") 230 | http.run(data=card_json, headers={"Content-type": "application/json"}) 231 | 232 | logging.info("Webhook request sent to MS Teams") 233 | -------------------------------------------------------------------------------- /sample_dag.py: -------------------------------------------------------------------------------- 1 | import pendulum 2 | 3 | from airflow.decorators import dag, task 4 | 5 | from ms_teams_powerautomate_webhook_operator import MSTeamsPowerAutomateWebhookOperator 6 | 7 | 8 | @dag( 9 | schedule=None, 10 | start_date=pendulum.datetime(2021, 1, 1, tz="UTC"), 11 | catchup=False, 12 | tags=["example"], 13 | ) 14 | def sample_dag(): 15 | 16 | @task() 17 | def get_formatted_date(**kwargs): 18 | iso8601date = kwargs["execution_date"].strftime("%Y-%m-%dT%H:%M:%SZ") 19 | # Teams date/time formatting: https://learn.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features#datetime-example 20 | formatted_date = ( 21 | f"{{{{DATE({iso8601date}, SHORT)}}}} at {{{{TIME({iso8601date})}}}}" 22 | ) 23 | print(formatted_date) 24 | return formatted_date 25 | 26 | formatted_date = get_formatted_date() 27 | 28 | op1 = MSTeamsPowerAutomateWebhookOperator( 29 | task_id="send_to_teams", 30 | http_conn_id="msteams_webhook_url", 31 | heading_title="Airflow local", 32 | header_bar_style="good", 33 | heading_subtitle=formatted_date, 34 | card_width_full=False, 35 | body_message="""Dag **lorem_ipsum** has completed successfully in **localhost**""", 36 | body_facts_dict={"Lorems": "184", "Dolor": "Sat", "Time taken": "2h 30m"}, 37 | button_text="View logs", 38 | ) 39 | 40 | formatted_date >> op1 41 | 42 | # op1 = MSTeamsPowerAutomateWebhookOperator( 43 | # task_id="send_to_teams", 44 | # http_conn_id="msteams_webhook_url", 45 | # card_width_full=True, 46 | # header_bar_show=True, 47 | # header_bar_style="good", 48 | # heading_title="Message from Airflow Local", 49 | # heading_title_size="medium", 50 | # heading_subtitle=formatted_date, 51 | # heading_subtitle_subtle=True, 52 | # heading_show_logo=True, 53 | # body_message="**lorem_ipsum** has completed successfully in **localhost**", 54 | # body_message_color_type="positive", 55 | # button_text="View logs", 56 | # button_url="http://localhost:8080", 57 | # button_style="default", 58 | # button_show=True, 59 | # ) 60 | 61 | 62 | sample_dag() 63 | -------------------------------------------------------------------------------- /samplecard.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "message", 3 | "attachments": [ 4 | { 5 | "contentType": "application/vnd.microsoft.card.adaptive", 6 | "contentUrl": null, 7 | "content": { 8 | "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", 9 | "type": "AdaptiveCard", 10 | "msteams": { 11 | "width": "Full" 12 | }, 13 | "version": "1.0", 14 | "body": [ 15 | { 16 | "type": "Container", 17 | "style": "good", 18 | "bleed": true, 19 | "spacing": "None", 20 | "items": [ 21 | { 22 | "type": "ColumnSet", 23 | "columns": [ 24 | { 25 | "type": "Column", 26 | "width": "auto", 27 | "items": [ 28 | { 29 | "type": "Image", 30 | "url": "data:image/webp;base64,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", 31 | "altText": "Airflow logo", 32 | "size": "small", 33 | "style": "default" 34 | } 35 | ] 36 | }, 37 | { 38 | "type": "Column", 39 | "width": "stretch", 40 | "items": [ 41 | { 42 | "type": "TextBlock", 43 | "text": "Airflow Localhost", 44 | "weight": "bolder", 45 | "wrap": true 46 | }, 47 | { 48 | "type": "TextBlock", 49 | "spacing": "none", 50 | "text": "{{DATE(2023-09-15T08:08:15Z, SHORT)}} at {{TIME(2023-09-15T08:08:15Z)}}", 51 | "isSubtle": true, 52 | "wrap": true 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | { 61 | "type": "Container", 62 | "items": [ 63 | { 64 | "type": "TextBlock", 65 | "text": "**lorem_ipsum_dolor** ran successfully in **localhost** environment.", 66 | "wrap": true, 67 | "color": "warning" 68 | } 69 | ] 70 | } 71 | ], 72 | "actions": [ 73 | { 74 | "type": "Action.OpenUrl", 75 | "title": "View Logs", 76 | "url": "https://example.com" 77 | } 78 | ] 79 | } 80 | } 81 | ] 82 | } -------------------------------------------------------------------------------- /screenshots/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendhak/Airflow-MS-Teams-Operator/3361c32cd069976793d640343037943551595ec8/screenshots/001.png -------------------------------------------------------------------------------- /screenshots/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendhak/Airflow-MS-Teams-Operator/3361c32cd069976793d640343037943551595ec8/screenshots/002.png -------------------------------------------------------------------------------- /screenshots/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendhak/Airflow-MS-Teams-Operator/3361c32cd069976793d640343037943551595ec8/screenshots/003.png -------------------------------------------------------------------------------- /screenshots/004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendhak/Airflow-MS-Teams-Operator/3361c32cd069976793d640343037943551595ec8/screenshots/004.png -------------------------------------------------------------------------------- /screenshots/005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendhak/Airflow-MS-Teams-Operator/3361c32cd069976793d640343037943551595ec8/screenshots/005.png -------------------------------------------------------------------------------- /screenshots/006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendhak/Airflow-MS-Teams-Operator/3361c32cd069976793d640343037943551595ec8/screenshots/006.png --------------------------------------------------------------------------------