<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.m-tari.com/feed.xml" rel="self" type="application/atom+xml"/><link href="https://www.m-tari.com/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-07-28T18:56:56+00:00</updated><id>https://www.m-tari.com/feed.xml</id><title type="html">blank</title><subtitle>Co-founder of Phrasel. Building AI products for language learning, with a background in NLP and computational fluid dynamics. </subtitle><entry><title type="html">Send scheduled messages from a bot to a channel in Microsoft Teams</title><link href="https://www.m-tari.com/writing/2024/send-scheduled-messages-teams/" rel="alternate" type="text/html" title="Send scheduled messages from a bot to a channel in Microsoft Teams"/><published>2024-09-15T16:00:00+00:00</published><updated>2024-09-15T16:00:00+00:00</updated><id>https://www.m-tari.com/writing/2024/send-scheduled-messages-teams</id><content type="html" xml:base="https://www.m-tari.com/writing/2024/send-scheduled-messages-teams/"><![CDATA[<p>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 <a href="https://github.com/microsoft/BotBuilder-Samples">botbuilder repository</a> provides several great examples such as <a href="https://github.com/OfficeDev/Microsoft-Teams-Samples/tree/main/samples/bot-initiate-thread-in-channel/python">Starting a new thread in a channel</a>, I found that I needed a few extra steps to adapt those samples to my use case.</p> <p>One important difference is that most examples rely on the <code class="language-plaintext highlighter-rouge">on_message_activity</code> trigger, which works well for user-driven interactions. In my case, however, I needed something that runs independently of incoming messages.</p> <p>To handle scheduling, I decided to use Celery as a task scheduler. I also used the <a href="https://github.com/Microsoft/botbuilder-python">botbuilder connector</a> library so I could interact with the Microsoft Teams API outside of the bot’s main controller.</p> <p>Here are the libraries used in this setup:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>botbuilder-core==4.14.6
celery==5.3.4
</code></pre></div></div> <p>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 <a href="https://pypi.org/project/botframework-connector/">botframework-connector</a> and modified it to work with Microsoft Teams channels.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">botbuilder.schema</span> <span class="kn">import</span> <span class="n">ConversationParameters</span><span class="p">,</span> <span class="n">ChannelAccount</span>
<span class="kn">from</span> <span class="n">botframework.connector</span> <span class="kn">import</span> <span class="n">ConnectorClient</span>
<span class="kn">from</span> <span class="n">botframework.connector.auth</span> <span class="kn">import</span> <span class="n">MicrosoftAppCredentials</span>
<span class="kn">from</span> <span class="n">celery</span> <span class="kn">import</span> <span class="n">Celery</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">Celery</span><span class="p">(</span><span class="sh">'</span><span class="s">tasks</span><span class="sh">'</span><span class="p">,</span> <span class="n">broker</span><span class="o">=</span><span class="sh">'</span><span class="s">&lt;broker-address&gt;</span><span class="sh">'</span><span class="p">)</span>

<span class="n">APP_ID</span> <span class="o">=</span> <span class="sh">'</span><span class="s">&lt;your-app-id&gt;</span><span class="sh">'</span>
<span class="n">APP_PASSWORD</span> <span class="o">=</span> <span class="sh">'</span><span class="s">&lt;your-app-password&gt;</span><span class="sh">'</span>
<span class="n">SERVICE_URL</span> <span class="o">=</span> <span class="sh">'</span><span class="s">https://smba.trafficmanager.net/teams/</span><span class="sh">'</span>
<span class="n">CHANNEL_ID</span> <span class="o">=</span> <span class="sh">'</span><span class="s">msteams</span><span class="sh">'</span>
<span class="n">BOT_ID</span> <span class="o">=</span> <span class="sh">'</span><span class="s">&lt;bot-id&gt;</span><span class="sh">'</span> <span class="c1"># for MS Teams app, it's '28:APP_ID'
</span><span class="n">TEAMS_CHANNEL_ID</span> <span class="o">=</span> <span class="sh">'</span><span class="s">&lt;channel-id&gt;</span><span class="sh">'</span>

<span class="n">credentials</span> <span class="o">=</span> <span class="nc">MicrosoftAppCredentials</span><span class="p">(</span><span class="n">APP_ID</span><span class="p">,</span> <span class="n">APP_PASSWORD</span><span class="p">)</span>
<span class="n">connector</span> <span class="o">=</span> <span class="nc">ConnectorClient</span><span class="p">(</span><span class="n">credentials</span><span class="p">,</span> <span class="n">base_url</span><span class="o">=</span><span class="n">SERVICE_URL</span><span class="p">)</span>

<span class="nd">@app.task</span>
<span class="k">def</span> <span class="nf">send_message_to_teams</span><span class="p">():</span>
    <span class="n">message</span> <span class="o">=</span> <span class="n">MessageFactory</span><span class="p">.</span><span class="nf">text</span><span class="p">(</span><span class="sh">"</span><span class="s">Hello World!</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">conversation</span> <span class="o">=</span> <span class="n">connector</span><span class="p">.</span><span class="n">conversations</span><span class="p">.</span><span class="nf">create_conversation</span><span class="p">(</span>
        <span class="nc">ConversationParameters</span><span class="p">(</span>
            <span class="n">bot</span><span class="o">=</span><span class="nc">ChannelAccount</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="n">BOT_ID</span><span class="p">),</span>
            <span class="n">channel_data</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">channel</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">:</span> <span class="n">TEAMS_CHANNEL_ID</span><span class="p">}},</span>
            <span class="n">activity</span><span class="o">=</span><span class="n">message</span><span class="p">,</span>
        <span class="p">)</span>
    <span class="p">)</span>
</code></pre></div></div> <p>There is also another method to send a conversation to a channel, <code class="language-plaintext highlighter-rouge">connector.conversations.send_to_conversation()</code>, which does not work currently with channels in Microsoft Teams, so instead I add the <code class="language-plaintext highlighter-rouge">activity</code> argument to <code class="language-plaintext highlighter-rouge">connector.conversations.create_conversation()</code> so that it sends the message at the same time that it creates the conversation.</p> <p>To make this work, I needed the unique identifier of the target channel (<code class="language-plaintext highlighter-rouge">TEAMS_CHANNEL_ID</code>). The easiest way I found to retrieve it is by right-clicking the channel in Microsoft Teams and selecting <strong>Get Link to channel</strong>, then extracting the URL-encoded identifier. <a href="https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0">Microsoft Graph API</a> can also be used to list teams and channels:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET https://graph.microsoft.com/v1.0/teams
GET https://graph.microsoft.com/v1.0/teams/{team-id}/channels
</code></pre></div></div> <p>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.:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">celery.schedules</span> <span class="kn">import</span> <span class="n">crontab</span>

<span class="n">app</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="n">beat_schedule</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">'</span><span class="s">send-message-to-teams</span><span class="sh">'</span><span class="p">:</span> <span class="p">{</span>
        <span class="sh">'</span><span class="s">task</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">tasks.send_message_to_teams</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">schedule</span><span class="sh">'</span><span class="p">:</span> <span class="nf">crontab</span><span class="p">(</span><span class="n">hour</span><span class="o">=</span><span class="mi">7</span><span class="p">,</span> <span class="n">minute</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">day_of_week</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
    <span class="p">},</span>
<span class="p">}</span>
</code></pre></div></div> <p>Finally, I run two separate processes: one for the Celery Beat scheduler and one for the worker that executes the tasks.</p> <p>To start the scheduler:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>celery <span class="nt">-A</span> tasks beat <span class="nt">--loglevel</span><span class="o">=</span>INFO
</code></pre></div></div> <p>and a Celery worker that simply executes the task on our desired time:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>celery <span class="nt">-A</span> tasks worker <span class="nt">--loglevel</span><span class="o">=</span>INFO
</code></pre></div></div> <p>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!</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/2024-09-15-send-scheduled-messages-teams/teams-scheduled-message-480.webp 480w,/assets/img/2024-09-15-send-scheduled-messages-teams/teams-scheduled-message-800.webp 800w,/assets/img/2024-09-15-send-scheduled-messages-teams/teams-scheduled-message-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/2024-09-15-send-scheduled-messages-teams/teams-scheduled-message.png" class="img-fluid" width="100%" height="auto" alt="Scheduled bot message posted in a Microsoft Teams channel" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> </figure>]]></content><author><name></name></author><category term="writing"/><category term="microsoft-teams,"/><category term="bots,"/><category term="celery,"/><category term="python"/><summary type="html"><![CDATA[Using Celery and botframework-connector to post scheduled messages to Teams channels.]]></summary></entry></feed>