-
Notifications
You must be signed in to change notification settings - Fork 196
ZZZ ‐ [Archived] ‐ Send notification to Teams
Important
Content in this document has been moved to Teams platform documentation. Please do not refer to or update this document.
We appreciate your feedback, please report any issues to us here.
The Notification in Teams feature enables you to build applications that consume events and send these as notifications to an individual person, a chat, or a channel in Teams. Notifications can be sent as plain text or Adaptive Cards.
Get started with Teams Toolkit and TeamsFx SDK:
- How to create a new notification bot
- How to understand the notification bot project
- How notification works
Customize the scaffolded app template:
- How to customize the notification behavior
- How to customize the initialization
- How to customize the installation
- How to customize the adapter
- How to customize the storage
- How to add authentication for your notification API
- How to connect to an existing API
Extend notification bot to other bot scenarios:
Alternative ways to send notifications to Teams:
- From Teams Toolkit side bar click
Create a New App
or selectTeams: Create a New App
from the command palette.
- Select
Bot
.
- Select
Chat Notification Message
.
- Select triggers. You can choose from
HTTP Trigger
orTimer Trigger
. The triggers are based onRestify Server
(means the created app code is a restify web app) orAzure Functions
(means the created app code is Azure Functions).
- Select programming language
- Select a folder where to create you project. The default one is
${HOME}/TeamsApps/
.
- Enter an application name and then press enter.
-
Make sure you have installed ASP.NET workloads and "Microsoft Teams development tools".
-
Create a new project and select "Microsoft Teams App".
-
In next window enter your project name.
-
In next window select Notification Bot. Then select a trigger type in right panel. You can choose from HTTP Trigger or Timer Trigger. The triggers are based on ASP.NET Core Web API (means the created app code is a web app) or Azure Functions (means the created app code is Azure Functions)..
-
If you prefer interactive mode, execute
teamsfx new
command, then use the keyboard to go through the same flow as in Visual Studio Code. -
If you prefer non-interactive mode, enter all required parameters in one command.
teamsfx new --interactive false --capabilities "notification" --bot-host-type-trigger "http-restify" --programming-language "typescript" --folder "./" --app-name MyAppName
After you successfully created the project, you can quickly start local debugging via F5
in VSCode. Select Debug (Edge)
or Debug (Chrome)
debug option of your preferred browser. If you created a timer triggered notification, after running this template and you will get a notification as below:
The created app is a normal TeamsFx project that will contain following folders:
Folder | Contents |
---|---|
teamsapp.yml , teamsapp.local.yml , env/
|
Project level settings and configurations |
.vscode/ |
VSCode files for local debug |
appPackage/ |
Templates for Teams app manifest |
infra/ |
Templates for Azure resources |
src/ |
The source code |
If you select (Restify)
trigger(s), the src/
folder is restify web app with following content:
File / Folder | Contents |
---|---|
src/adaptiveCards/ |
Adaptive card templates |
src/internal/ |
Generated initialize code for notification functionality |
src/index.*s |
The entrypoint to handle bot messages and send notifications |
src/teamsBot.*s |
An empty implementation of Teams activity handler that can be extended to other bot scenarios |
If you select (Azure Functions)
trigger(s), the src/
folder contains azure functions code, and you will also see some other folders:
File / Folder | Contents |
---|---|
messageHandler/ |
The function to handle bot messages |
*Trigger/ |
The function to trigger notification |
src/adaptiveCards/ |
Adaptive card templates |
src/internal/ |
Generated initialize code for notification functionality |
src/*Trigger.*s |
The entrypoint of each notification trigger |
src/teamsBot.*s |
An empty implementation of Teams activity handler that can be extended to other bot scenarios |
.funcignore |
The azure functions ignore file to exclude local files |
host.json |
The azure functions host file |
local.settings.json |
The azure functions local setting file |
If you selected WebApi Http trigger, the project structure would like this:
Folder | Contents |
---|---|
teamsapp.yml , teamsapp.local.yml , env/
|
Project level settings and configurations |
Properties/ |
LaunchSetting file for local debug |
Controllers/ |
BotController and NotificationControllers to handle the message content |
Models/ |
Adaptive card data models |
Resources/ |
Adaptive card templates |
appPackage/ |
Templates for Teams app manifest |
infra/ |
Templates for Azure resources |
appsettings.*.json |
The runtime settings |
GettingStarted.txt |
Instructions on minimal steps to wonderful |
Program.cs |
Create the Teams Bot instance |
TeamsBot.cs |
An empty Bot handler |
If you selected Timer trigger, the project structure would be generally the same, only differences are listed in below table:
Folder | Contents |
---|---|
MessageHandler.cs |
Instead of Controller folder, MessageHandler is where you can define the message content |
NotifyTimerTrigger.cs |
Define the time interval to trigger the notification |
Startup.cs |
Instead of Program.cs, Startup.cs create the Teams Bot instance |
If you selected Azure Functions based HTTP trigger, the project structure would be generally the same, only differences are listed in below table:
Folder | Contents |
---|---|
MessageHandler.cs |
Start an asynchronized task to call http request |
NotifyHttpTrigger.cs |
Handle the request result and convert to message content |
Startup.cs |
Instead of Program.cs, Startup.cs create the Teams Bot instance |
Technically, Bot Framework SDK provides the functionality to proactively message in Teams. And TeamsFx SDK provides the functionality to manage bot's conversation references when bot event is triggered.
Current TeamsFx SDK recognize following bot events:
Event | Behavior |
---|---|
The first time Bot is added(installed) to Person/Group/Team | Add the target conversation reference to storage |
Bot is removed(uninstalled) from Person/Group/Team | Remove the target conversation reference from storage |
Team that bot installed in is deleted | Remove the target conversation reference from storage |
Team that bot installed in is restored | Add the target conversation reference to storage |
Bot is messaged/mentioned | Add the target conversation reference to storage if not exist |
When notifying, TeamsFx SDK creates new conversation from the selected conversation reference and send messages. Or, for advanced usage, you can directly access the conversation reference to execute your own bot logic:
/** Typescript **/
// list all installation targets
for (const target of await notificationApp.notification.installations()) {
// call Bot Framework's adapter.continueConversationAsync()
await target.adapter.continueConversationAsync(
target.botAppId,
target.conversationReference,
async (context) => {
// your own bot logic
await context...
}
);
}
/** .NET **/
// list all installation targets
foreach (var target in await _conversation.Notification.GetInstallationsAsync()) {
// call Bot Framework's adapter.ContinueConversationAsync()
await target.Adapter.ContinueConversationAsync(
target.BotAppId,
target.ConversationReference,
async (context, ctx) =>
{
// your own bot logic
await context...
},
cancellationToken);
}
There are few customizations you can make to extend the template to fit your business requirements.
- Step 1: Customize the trigger point from event source
- Step 2: Customize the notification content
- Step 3: Customize where notifications are sent
By default Teams Toolkit scaffolds a single restify
entry point in src/index.js
. When a HTTP request is sent to this entry point, the default implementation sends a hard-coded Adaptive Card to Teams. You can customize this behavior by customizing src/index.js
. A typical implementation might make an API call to retrieve some events and/or data, and then send an Adaptive Card as appropriate.
You can also add additional triggers by:
- Creating new routing:
server.post("/api/new-trigger", ...);
- Add Timer trigger(s) via widely-used npm packages such as cron, node-schedule, etc. Or add other trigger(s) via other packages.
If you selected timer
trigger, the default Azure Function timer trigger (src/timerTrigger.ts
) implementation simply sends a hard-coded Adaptive Card every 30 seconds. You can edit the file *Trigger/function.json
to customize the schedule
property. Refer to the Azure Function documentation for more details.
If you selected http
trigger, when this trigger is hit (via a HTTP request), the default implementation sends a hard-coded Adaptive Card to Teams. You can change this behavior by customizing src/*Trigger.ts
. A typical implementation might make an API call to retrieve some events and/or data, and then send an Adaptive Card as appropriate.
You can also add any Azure Function trigger. For example:
- You can use an
Event Hub
trigger to send notifications when an event is pushed to Azure Event Hub. - You can use a
Cosmos DB
trigger to send notifications when a Cosmos document has been created or updated.
See Azure Functions supported triggers.
src/adaptiveCards/notification-default.json
defines the default Adaptive Card. You can use the Adaptive Card Designer to help visually design your Adaptive Card UI.
src/cardModels.ts
defines a data structure that is used to fill data for the Adaptive Card. The binding between the model and the Adaptive Card is done by name matching (for example,CardData.title
maps to ${title}
in the Adaptive Card). You can add, edit, or remove properties and their bindings to customize the Adaptive Card to your needs.
You can also add new cards if needed. Follow this sample to see how to build different types of adaptive cards with a list or a table of dynamic contents using ColumnSet
and FactSet
.
/** Typescript **/
// list all installation targets
for (const target of await notificationApp.notification.installations()) {
// "Channel" means this bot is installed to a Team (default to notify General channel)
if (target.type === "Channel") {
// Directly notify the Team (to the default General channel)
await target.sendAdaptiveCard(...);
// List all members in the Team then notify each member
const members = await target.members();
for (const member of members) {
await member.sendAdaptiveCard(...);
}
// List all channels in the Team then notify each channel
const channels = await target.channels();
for (const channel of channels) {
await channel.sendAdaptiveCard(...);
}
}
}
/** .NET **/
// list all installation targets
foreach (var target in await _conversation.Notification.GetInstallationsAsync()) {
// "Channel" means this bot is installed to a Team (default to notify General channel)
if (target.Type == NotificationTargetType.Channel)
{
// Directly notify the Team (to the default General channel)
await target.SendAdaptiveCard(...);
// List all members in the Team then notify each member
var members = await target.GetMembersAsync();
foreach (var member in members) {
await member.SendAdaptiveCard(...);
}
// List all channels in the Team then notify each channel
var channels = await target.GetChannelsAsync();
foreach (var channel in channels) {
await channel.SendAdaptiveCard(...);
}
}
}
/** Typescript **/
// list all installation targets
for (const target of await notificationApp.notification.installations()) {
// "Group" means this bot is installed to a Group Chat
if (target.type === "Group") {
// Directly notify the Group Chat
await target.sendAdaptiveCard(...);
// List all members in the Group Chat then notify each member
const members = await target.members();
for (const member of members) {
await member.sendAdaptiveCard(...);
}
}
}
/** .NET **/
// list all installation targets
foreach (var target in await _conversation.Notification.GetInstallationsAsync()) {
// "Group" means this bot is installed to a Group Chat
if (target.Type == NotificationTargetType.Group)
{
// Directly notify the Group Chat
await target.SendAdaptiveCard(...);
// List all members in the Group Chat then notify each member
var members = await target.GetMembersAsync();
foreach (var member in members) {
await member.SendAdaptiveCard(...);
}
}
}
/** Typescript **/
// list all installation targets
for (const target of await notificationApp.notification.installations()) {
// "Person" means this bot is installed as Personal app
if (target.type === "Person") {
// Directly notify the individual person
await target.sendAdaptiveCard(...);
}
}
/** .NET **/
// list all installation targets
foreach (var target in await _conversation.Notification.GetInstallationsAsync()) {
// "Person" means this bot is installed as Personal app
if (target.Type == NotificationTargetType.Person)
{
// Directly notify the individual person
await target.SendAdaptiveCard(...);
}
}
Note: It's required to install the bot app into the 'General' channel of a team, otherwise undefined will be returned.
/** Typescript **/
// find the first channel when the predicate is true.
const channel = await notificationApp.notification.findChannel(c => Promise.resolve(c.info.name === "MyChannelName"));
// send adaptive card to the specific channel.
await channel?.sendAdaptiveCard(...);
Note: The specified person should belongs to the notification installation scope, otherwise it can't be found and undefined will be returned.
/** Typescript **/
// find the first person when the predicate is true.
const member = await notificationApp.notification.findMember(m => Promise.resolve(m.account.name === "Bob"));
// send adaptive card to the specific person.
await member?.sendAdaptiveCard(...);
To send notification, you need to create ConversationBot
first. (Code already generated in project)
/** Javascript/Typescript: src/internal/initialize.*s **/
const notificationApp = new ConversationBot({
// The bot id and password to create CloudAdapter.
// See https://aka.ms/about-bot-adapter to learn more about adapters.
adapterConfig: {
MicrosoftAppId: config.botId,
MicrosoftAppPassword: config.botPassword,
MicrosoftAppType: "MultiTenant",
},
// Enable notification
notification: {
enabled: true,
},
});
/** .NET: Program.cs or Startup.cs **/
// Create the Conversation with notification feature enabled.
builder.Services.AddSingleton(sp =>
{
var options = new ConversationOptions()
{
// To use your own CloudAdapter
Adapter = sp.GetService<CloudAdapter>(),
Notification = new NotificationOptions
{
BotAppId = builder.Configuration["MicrosoftAppId"],
},
};
return new ConversationBot(options);
});
A Teams bot can be installed into a team, or a group chat, or as personal app, depending on difference scopes. You can choose the installation target when adding the App.
-
See Distribute your app for more install options.
-
See Remove an app from Teams for uninstallation.
You can initialize with your own adapter, or customize after initialization.
/** Typescript **/
// Create your own adapter
const adapter = new CloudAdapter(...);
// Customize your adapter, e.g., error handling
adapter.onTurnError = ...
const notificationApp = new ConversationBot({
// use your own adapter
adapter: adapter;
...
});
// Or, customize later
notificationApp.adapter.onTurnError = ...
You can initialize with your own storage. This storage will be used to persist notification connections.
/** Typescript **/
// implement your own storage
class MyStorage implements NotificationTargetStorage {...}
const myStorage = new MyStorage(...);
// initialize ConversationBot with notification enabled and customized storage
const notificationApp = new ConversationBot({
// The bot id and password to create CloudAdapter.
// See https://aka.ms/about-bot-adapter to learn more about adapters.
adapterConfig: {
MicrosoftAppId: config.botId,
MicrosoftAppPassword: config.botPassword,
MicrosoftAppType: "MultiTenant",
},
// Enable notification
notification: {
enabled: true,
storage: myStorage,
},
});
/** .NET **/
// implement your own storage
public class MyStorage : INotificationTargetStorage {...}
// initialize ConversationBot with notification enabled and customized storage
builder.Services.AddSingleton(sp =>
{
var options = new ConversationOptions()
{
Adapter = sp.GetService<CloudAdapter>(),
Notification = new NotificationOptions
{
BotAppId = builder.Configuration["MicrosoftAppId"],
// Use your own storage
Storage = new MyStorage(),
},
};
return new ConversationBot(options);
});
This Sample provides a sample implementation that persists to Azure Blob Storage.
Note-1: It's recommended to use your own shared storage for production environment. If
storage
is not provided, a default local file storage will be used, which stores notification connections into:
- .notification.localstore.json if running locally
- ${process.env.TEMP}/.notification.localstore.json if
process.env.RUNNING_ON_AZURE
is set to "1"
Note-2: The
NotificationTargetStorage
is different from Bot Framework SDK's Custom Storage.
The notification storage requiresread
,write
,delete
,list
functionalities but Bot Framework SDK's storage usually only hasread
,write
,delete
, nolist
.
So, if you already implement your own Bot Framework SDK's storage, e.g.,botbuilder-azure-blobs.BlobsStorage
, you may still need to implement another for notification. They can share the same Blob Connection String but use different Containers.
If you choose http trigger, the scaffolded notification API does not have authentication / authorization enabled. We suggest you add authentication / authorization for this API before using it for production purpose. Here're some common ways to add authentication / authorization for an API:
-
Use an API Key. If you chose Azure Functions to host your notification bot, it already provides function access keys, which may be helpful to you.
-
Use an access token issued by Microsoft Entra
There would be more authentication / authorization solutions for an API. You can choose the one that satisfies your requirement best.
If you want to invoke external APIs in your code but do not have the appropriate SDK, the Integration API Connection document would be helpful to bootstrap code to call target APIs. You can also view the document via "Teams: View How-to Guides" command in Teams Toolkit VS Code extension.
One possible cause is that the installation event was omitted or did not reached to the bot service. Teams only send such event at the first installation time, so if the bot app was already installed before your notification bot service is launched, you are probably in this case.
There are two options to fix this:
- Send a message to your Personal bot or mention your bot in GroupChat / Channel. (This is to re-reach the bot service with correct installation information)
- Uninstall the bot app from Teams then re-debug/re-launch again. (This is to re-send the installation event to bot service)
Technically, notification target connections are stored in the persistence storage. If you are using the default local file storage, all installations will be stored under .notification.localstore.json
. Or refer to Customize Storage to add your own storage.
One possible cause is that the bot id or password is changed (usually due to cleaning local state or re-provisioning). E.g., if the notification installation does not match the bot (id/password) you are running, you may get a "Failed to decrypt conversation id" error.
And a quick fix could be:
-
Clean your notification storage (by default for local case it's
.notification.localstore.json
)After cleaning, message or re-install your bot in Teams to ensure the new installation is up-to-date.
Technically, each stored notification installation is bound with one bot. If you are able to check your notification storage, its bot
field should match the bot you are running (E.g., the bot id contains the same GUID).
Notification target connections are stored in the persistence storage. If you are using the default local file storage, Azure Web App and Azure Functions will clean up the local file when restart / redeploy.
It's recommended to use your own shared storage for production environment. See Customize Storage.
Or, as a workaround, after restart / redeploy, uninstall the bot from Teams, then re-install it to re-add connections to the storage.
One possible cause is that the bot app is installed into other channels instead of the General
channel.
And a quick fix could be:
-
Uninstall the bot app from Teams then re-debug/re-launch again.
Ensure the bot app is installed into the
General
channel after re-debugged/re-launched.
There are Microsoft Graph APIs to list apps installed in a team / group / chat. So it may require you to iterate all your teams / groups / chats to get all the targets a certain app is installed in.
In the notification project, it uses persistence storage to store installation targets. See How Notification Works for more information.
If azurite exits due to port in use, you can specify another listening port and update the connection string of AzureWebJobsStorage
in local.settings.json
-
Go to
src\internal\initialize.ts(js)
, update yourconversationBot
initialization to enable command-response feature: -
Follow this instruction to add command to your bot.
The Adaptive Card action handler feature enables the app to respond to adaptive card actions that triggered by end users to complete a sequential workflow. When user gets an Adaptive Card, it can provide one or more buttons in the card to ask for user's input, do something like calling some APIs, and then send another adaptive card in conversation to response to the card action.
To add adaptive card actions to command bot, you can follow the steps here.
Microsoft Teams Framework (TeamsFx) supports two major ways to help you send notifications from your system to Teams by creating a Teams Bot Application or Teams Incoming Webhook.
Here's the comparison of the two approaches to help you make the decision.
Teams Bot App | Teams Incoming Webhook | |
---|---|---|
Able to message individual person | Yes | No |
Able to message group chat | Yes | No |
Able to message public channel | Yes | Yes |
Able to message private channel | No | Yes |
Able to send card message | Yes | Yes |
Able to send welcome message | Yes | No |
Able to retrieve Teams context | Yes | No |
Require installation step on Teams | Yes | No |
Require Azure resource | Azure Bot Service | No |
Incoming Webhooks help in posting messages from apps to Teams. If Incoming Webhooks are enabled for a team in any channel, it exposes the HTTPS endpoint, which accepts correctly formatted JSON and inserts the messages into that channel. For example, you can create an Incoming Webhook in your DevOps channel, configure your build, and simultaneously deploy and monitor services to send alerts.
Teams Framework has built a sample that walks you through:
- How to create an incoming webhook in Teams.
- How to send notifications using incoming webhooks with adaptive cards.
Build Custom Engine Copilots
- Build a basic AI chatbot for Teams
- Build an AI agent chatbot for Teams
- Expand AI bot's knowledge with your content
Scenario-based Tutorials
- Send notifications to Teams
- Respond to chat commands in Teams
- Respond to card actions in Teams
- Embed a dashboard canvas in Teams
Extend your app across Microsoft 365
- Teams tabs in Microsoft 365 and Outlook
- Teams message extension for Outlook
- Add Outlook Add-in to a Teams app
App settings and Microsoft Entra Apps
- Manage Application settings with Teams Toolkit
- Manage Microsoft Entra Application Registration with Teams Toolkit
- Use an existing Microsoft Entra app
- Use a multi-tenant Microsoft Entra app
Configure multiple capabilities
- How to configure Tab capability within your Teams app
- How to configure Bot capability within your Teams app
- How to configure Message Extension capability within your Teams app
Add Authentication to your app
- How to add single sign on in Teams Toolkit for Visual Studio Code
- How to enable Single Sign-on in Teams Toolkit for Visual Studio
Connect to cloud resources
- How to integrate Azure Functions with your Teams app
- How to integrate Azure API Management
- Integrate with Azure SQL Database
- Integrate with Azure Key Vault
Deploy apps to production