Send scheduled messages from a bot to a channel in Microsoft Teams

A common task I ran into while building a bot for Microsoft Teams was sending messages from the bot to a channel on a schedule. While the Python version of the botbuilder repository provides several great examples such as Starting a new thread in a channel, I found that I needed a few extra steps to adapt those samples to my use case.

One important difference is that most examples rely on the on_message_activity trigger, which works well for user-driven interactions. In my case, however, I needed something that runs independently of incoming messages.

To handle scheduling, I decided to use Celery as a task scheduler. I also used the botbuilder connector library so I could interact with the Microsoft Teams API outside of the bot’s main controller.

Here are the libraries used in this setup:

botbuilder-core==4.14.6
celery==5.3.4

One challenge I ran into was related to authentication and message delivery. While it is possible to send messages to a channel using the Microsoft Graph API with delegated permissions, the same approach is not currently supported with application-level permissions for this scenario. Because of that, I adapted code from botframework-connector and modified it to work with Microsoft Teams channels.

from botbuilder.schema import ConversationParameters, ChannelAccount
from botframework.connector import ConnectorClient
from botframework.connector.auth import MicrosoftAppCredentials
from celery import Celery

app = Celery('tasks', broker='<broker-address>')

APP_ID = '<your-app-id>'
APP_PASSWORD = '<your-app-password>'
SERVICE_URL = 'https://smba.trafficmanager.net/teams/'
CHANNEL_ID = 'msteams'
BOT_ID = '<bot-id>' # for MS Teams app, it's '28:APP_ID'
TEAMS_CHANNEL_ID = '<channel-id>'

credentials = MicrosoftAppCredentials(APP_ID, APP_PASSWORD)
connector = ConnectorClient(credentials, base_url=SERVICE_URL)

@app.task
def send_message_to_teams():
    message = MessageFactory.text("Hello World!")
    conversation = connector.conversations.create_conversation(
        ConversationParameters(
            bot=ChannelAccount(id=BOT_ID),
            channel_data={"channel": {"id": TEAMS_CHANNEL_ID}},
            activity=message,
        )
    )

There is also another method to send a conversation to a channel, connector.conversations.send_to_conversation(), which does not work currently with channels in Microsoft Teams, so instead I add the activity argument to connector.conversations.create_conversation() so that it sends the message at the same time that it creates the conversation.

To make this work, I needed the unique identifier of the target channel (TEAMS_CHANNEL_ID). The easiest way I found to retrieve it is by right-clicking the channel in Microsoft Teams and selecting Get Link to channel, then extracting the URL-encoded identifier. Microsoft Graph API can also be used to list teams and channels:

GET https://graph.microsoft.com/v1.0/teams
GET https://graph.microsoft.com/v1.0/teams/{team-id}/channels

Next, I configured Celery Beat to schedule the message. In my case, I wanted the bot to send a message every Monday at 7:30 a.m.:

from celery.schedules import crontab

app.conf.beat_schedule = {
    'send-message-to-teams': {
        'task': 'tasks.send_message_to_teams',
        'schedule': crontab(hour=7, minute=30, day_of_week=1),
    },
}

Finally, I run two separate processes: one for the Celery Beat scheduler and one for the worker that executes the tasks.

To start the scheduler:

celery -A tasks beat --loglevel=INFO

and a Celery worker that simply executes the task on our desired time:

celery -A tasks worker --loglevel=INFO

Once everything is running, triggering the task manually from the application successfully posts a message to the Microsoft Teams channel. It was a small but satisfying milestone seeing the bot deliver scheduled messages without any user interaction!

Scheduled bot message posted in a Microsoft Teams channel