Table of Contents

  1. Install Botkit on your computer

  2. Create a Botkit powered Node app:

    botkit new --platform teams
  3. Follow this guide to configuring the Teams API

To connect Botkit to Teams, use the constructor method, Botkit.slackbot(). This will create a Botkit controller with all core features as well as some additional methods.

Argument Description
config an object containing configuration options

This function returns a Teams-ready Botkit controller. The values for clientId and clientSecret must be acquired from Bot Framework.

The config argument is an object with these properties:

Argument Description
studio_token String
debug Boolean
clientId The application' client id, provided by Bot Framework
clientSecret The application's client secret, provided by Bot Framework
var controller = Botkit.teamsbot({
    debug: true,
    log: true,
    clientId: process.env.clientId,
    clientSecret: process.env.clientSecret
});

In addition to sending and receiving chat messages, Botkit bots can use all of the other features in the Microsoft Teams API. With these other features, Botkit bots can send rich attachments with interactive buttons, integrate into the message composer, and expose integrated tab applications that live inside the Teams window and share data with the bot.

In addition to the core events that Botkit fires, this connector also fires some platform specific events.

The full list and payload schema of these events is available from Microsoft.

Event Description
direct_message the bot received a 1:1 direct message from a user
direct_mention the bot was addressed directly in a mult-user channel ("@bot hello!")
mention the bot was mentioned by someone in a message ("hello everyone including @bot")

Event Description
bot_channel_join the bot has joined a channel
user_channel_join a user has joined a channel
bot_channel_leave the bot has left a channel
user_channel_leave a user has left a channel

Event Description
channelDeleted a channel was deleted
channelRenamed a channel was renamed
channelCreated a new channel was created

Event Description
invoke a user clicked an invoke button See Buttons
composeExtension user submitted a query with the compose extension See Compose Extensions

The full code for a simple Microsoft Teams bot is below:

var Botkit = require('botkit');

var controller = Botkit.teamsbot({
  clientId: process.env.clientId,
  clientSecret: process.env.clientSecret,
});

controller.setupWebserver(process.env.PORT || 3000, function(err, webserver) {
    controller.createWebhookEndpoints(webserver, function() {
        console.log("BOTKIT: Webhooks set up!");
    });
});

controller.hears('hello', 'direct_message,direct_mention', function(bot, message) {
    bot.reply(message, 'Hi');
});

controller.on('direct_mention', function(bot, message) {
    bot.reply(message, 'You mentioned me and said, "' + message.text + '"');
});

controller.on('direct_message', function(bot, message) {
    bot.reply(message, 'I got your private message. You said, "' + message.text + '"');
});

Before your bot application can be used, you must prepare an "App Package" - a zip file containing a JSON file of configuration options, and (optionally) icons for your bot to use inside the Teams interface. This file must then be "sideloaded" into your Microsoft Teams account - this is just a fancy way of saying that you will have to upload this file into a settings page.

The manifest.json file is a hefty document, with lots of options! Here is the full documentation from Microsoft. We highly recommend using Botkit Studio to build your app package, as we have provided an easy to use tool to configure and generate the necessary file!

Here is a COMPLETE SAMPLE

Manifest.json schema docs

How to sideload your app

The Microsoft Teams API provides a number of features the bot developer can use to power a useful bot application that operates seamlessly in Teams.

Parameter Description
conversationId Contains the unique identifier of a conversation
userId The unique identifier for a given user
cb Callback function in the form function(err, user_profile)

getUserById takes elements from an incoming message object, and returns the user profile data associated with the message's sender.

controller.hears('who am i', 'direct_message, direct_mention', function(bot, message) {
    bot.api.getUserById(message.channel, message.user, function(err, user) {
        if (err) {
          bot.reply(message,'Error loading user:' + err);
        } else {
          bot.reply(message,'You are ' + user.name + ' and your email is ' + user.email + ' and your user id is ' + user.id);
        }
    });
});

Parameter Description
conversationId Contains the unique identifier of a conversation
upn The User Principal Name of a given team member
cb Callback function in the form function(err, user_profile)

This function is identical to getUserById(), but instead of fetching the user by the Teams-only user ID, it uses the user's "universal principal name" or "UPN", which defines the account in terms of the broader Microsoft Office ecosystem. This function is useful when connecting users in Microsoft Teams chat with the same users in a Tab Application, as tab applications only expose the upn value.

The Botkit Starter Kit for Microsoft Teams includes a sample middleware that uses this function to automatically translate the Teams-only ID into a UPN for use with the built-in storage system.

Parameter Description
conversationId Contains the unique identifier of a conversation
cb Callback function in the form function(err, members)

This function returns a list of members in the specified channel - either a 1:1 channel, or a multi-user team channel. This API returns an array of user profile objects identical to those returned by getUserById() and getUserByUpn().

controller.hears('get members','direct_mention,direct_message', function(bot, message) {
  bot.api.getConversationMembers(message.channel, function(err, roster) {
    if (err) {
      bot.reply(message,'Error loading roster: ' + err);
    } else {

      var list = [];
      for (var u = 0; u < roster.length; u++) {
        list.push(roster[u].name);
      }
      bot.reply(message,'Conversation members: ' + list.join(', '));
    }
  });
});

Parameter Description
teamId The unique identifier for a given team
cb Callback function in the form function(err, members)

This function works just like getConversationMembers(), but returns all members of a team instead of just the members of a specific channel.

The teamId, when present, can be extracted from a message object at the Teams-specific field message.channelData.team.id. This field is present in messages that occur in multi-user channels, but not in 1:1 messages and other events.

Note that since the team id is not always part of the incoming message payload, and because all multi-user channel contain all members of the team, getConversationMembers() is likely more reliable and easy to use.

controller.hears('roster','direct_mention', function(bot, message) {
  bot.api.getTeamRoster(message.channelData.team.id, function(err, roster) {
    if (err) {
      bot.reply(message,'Error loading roster: ' + err);
    } else {
      var list = [];
      for (var u = 0; u < roster.length; u++) {
        list.push(roster[u].name);
      }
      bot.reply(message,'Team roster: ' + list.join(', '));
    }
  });

});

Parameter Description
conversationId Contains the identifier for the conversation in which the original message occured
messageId Contains the unique identifier of message to be replaced
replacement A message object which will be used to replace the previous message
cb Callback function in the form function(err, results)

This method allows you to update an existing message with a replacement. This is super handy when responding to button click events, or updating a message with new information.

In order to update a message, you must first capture it's ID. The message id is part of the response passed back from bot.reply or bot.say.

updateMessage() expects an API-ready message object - the replacement message does not undergo the normal pre-send transformations that occur during a normal bot.reply or bot.say.

  controller.hears('update', 'direct_message,direct_mention', function(bot, message) {
      bot.reply(message,'This is the original message', function(err, outgoing_message) {
          bot.api.updateMessage(message.channel, outgoing_message.id, {type: 'message', text: 'This message has UPDATED CONTENT'}, function(err) {
            if (err) {
              console.error(err);
            }
          });
      });
  })

Parameter Description
teamId The unique identifier for a given team
cb Callback function in the form function(err, channels)

This function returns an array of all the channels in a given team.

The teamId, when present, can be extracted from a message object at the Teams-specific field message.channelData.team.id. This field is present in messages that occur in multi-user channels, but not in 1:1 messages and other events.

  controller.hears('channels','direct_mention', function(bot, message) {
    bot.api.getChannels(message.channelData.team.id, function(err, roster) {
      if (err) {
        bot.reply(message,'Error loading channel list: ' + err);
      } else {
        var list = [];
        for (var u = 0; u < roster.length; u++) {
          list.push(bot.channelLink(roster[u]));
        }
        bot.reply(message,'Channels: ' + list.join(', '));
      }
    });
  });

Parameter Description
conversationId Contains the unique identifier of a conversation
message The contents of your message
cb Callback function in the form function(err, results)

This function is used to send messages to Teams. It is used internally by Botkit's bot.say() function, and is not intended to be used directly by developers.

Parameter Description
options an object containing {bot: id, members: [], channelData: {}}
cb Callback function in the form function(err, new_conversation_object)

This function creates a new conversation context inside Teams. This is used internally by Botkit inside functions like startPrivateConversation() (to create the 1:1 channel between user and bot). It is not intended to be used directly by developers.

In addition to, or as an alternative to text, messages in Microsoft Teams can include one or more attachments. Attachments appear as interactive cards inside the Teams client, and can include elements such as images, text, structured data, and interactive buttons.

Read the official Teams documentation about message attachments

To use attachments with Botkit, construct an attachment object and add it to the message object. Botkit provides a few helper functions to make creating attachment objects easier.

Parameter Description
title OR object string value for the title of the card, OR an object representing all the fields in the card
subtitle string value for the subtitle of the card
text string value for the text of the card
images an array of image objects - {url: string, alt: string} - currently limited to 1 item
buttons an array of action objects - {type: string, title: string, value: string}
tap action a single of action object that defines the action to take when a user taps anywhere on the card - {type: string, title: string, value: string}

(See usage notes below)

Parameter Description
title OR object string value for the title of the card, OR an object representing all the fields in the card
subtitle string value for the subtitle of the card
text string value for the text of the card
images an array of image objects - {url: string, alt: string} - currently limited to 1 item
buttons an array of action objects - {type: string, title: string, value: string}
tap action a single of action object that defines the action to take when a user taps anywhere on the card - {type: string, title: string, value: string}

The attachment building helper functions bot.createHero() and bot.createThumbnail() can be used to quickly create attachment objects for inclusion in a message.

The return value of these functions is an attachment object that can be directly added to the outgoing message object's attachments array. In addition, the returned attachment object has a few helper methods of its that allow developers to adjust the values:

Parameter Description
value new value for the title field

Parameter Description
value new value for the subtitle field

Parameter Description
value new value for the text field

Parameter Description
url url to image
alt alt description for image

Parameter Description
type OR button object type of button OR an button object {type: string, title: string, value: string}
title string value for the button title
value string for the object payload.

Parameter Description
type OR button object new value for the title field
title string value for the action title
value string for the object payload.

Returns the stringified version of the attachment object

These functions can be used in a few different ways:

Create attachment with individual parameters:

var reply = {
  text: 'Here is an attachment!',
  attachments: [],
}

var attachment = bot.createHero('Title','subtitle','text',[{url:'http://placeimg.com/1900/600'}],[{type:'imBack','title':'Got it','value':'acknowledged'}],{type:'openUrl',value:'http://mywebsite.com'});

reply.attachments.push(attachment);

Create attachment with pre-defined object:

var reply = {
  text: 'Here is an attachment!',
  attachments: [],
}

var attachment = bot.createHero({
  title: 'My title',
  subtitle: 'My subtitle',
  text: 'My text',
});

reply.attachments.push(attachment);

Create attachment with helper methods:

var reply = {
  text: 'Here is an attachment!',
  attachments: [],
}

var attachment = bot.createHero();

attachment.title('This is the title');
attachment.text('I am putting some text into a hero card');
attachment.button('imBack','Click Me','I clicked a button!');
attachment.button('openUrl','Link Me','http://website.com');
attachment.button('invoke','Trigger Event',JSON.stringify({'key':'value'}));

reply.attachments.push(attachment);

When sending multiple attachments, you may want to specify the attachmentLayout attribute of the message object. Setting attachmentLayout to carousel will cause attachments to be displayed as a carousel, while the default behavior is to use a list layout.

controller.hears('hero',  'direct_mention, direct_message', function(bot, message) {

  // this is a sample message object with an attached hero card
  var reply = {
    "text": "Here is a hero card:",
    "attachments": [
      {
        "contentType": "application/vnd.microsoft.card.hero",
        "content": {
          "title": "Hero card title",
          "subtitle": "Hero card subtitle",
          "text": "The text of my hero card"
          "images": [
            {
              "url": "http://placeimg.com/1600/900",
              "alt": "An image from placeimg.com"
            }
          ],
        }
      }
    ]
  };

  bot.reply(message,reply);
});
controller.hears('thumbnail', 'direct_mention, direct_message',  function(bot, message) {

  // this is a sample message object with an attached hero card
  var reply = {
    "text": "Here is a thumbnail card:",
    "attachments": [
      {
        "contentType": "application/vnd.microsoft.card.thumbnail",
        "content": {
          "title": "Thumbnail title",
          "subtitle": "Thumbnail subtitle",
          "text": "The text of my Thumbnail card"
          "images": [
            {
              "url": "http://placeimg.com/900/900",
              "alt": "A nice square image from placeimg.com"
            }
          ],
        }
      }
    ]
  };

  bot.reply(message,reply);
});
controller.hears('image', 'direct_mention, direct_message', function(bot, message) {

  // this is a sample message object with an attached hero card
  var reply = {
    "text": "Check out this attached image!",
    "attachments": [
      {
        "contentType": "image/png",
        "contentUrl": "http://mywebsite.com/image.png",
      }
    ]
  };

  bot.reply(message,reply);
});

TODO

Buttons can be included in attachments. There are several types of button that result in different actions.

Note that is possible to send an attachment that is empty except for buttons - this can be useful!

To use buttons, build them with the attachment helpers, or construct them in code and include them in your attachment objects, as seen in the examples below:

controller.hears('invoke button', function(bot, message) {
var reply =
  {
    text: 'This message has an invoke button',
    attachments: [
      {
         "contentType": "application/vnd.microsoft.card.hero",
         "content": {
           // other card fields here, see above
           // title: '...',
           // subtitle: '...',
           // ...
           "buttons": [
             {
               "type": "invoke",
               "title": "Click Me",
               "value": "{\"action\":\"click\"}"
             }
           ]
      }
    ]
  };

  bot.reply(messge, reply);
});
controller.hears('imback button', function(bot, message) {
var reply =
  {
    text: 'This message has an imBack  button',
    attachments: [
      {
         "contentType": "application/vnd.microsoft.card.hero",
         "content": {
           // other card fields here, see above
           // title: '...',
           // subtitle: '...',
           // ...
           "buttons": [
             {
               "type": "imBack",
               "title": "Hello!",
               "value": "hello"
             }
           ]
      }
    ]
  };

  bot.reply(messge, reply);
});
controller.hears('openurl button', function(bot, message) {
var reply =
  {
    text: 'This message has an openUrl button',
    attachments: [
      {
         "contentType": "application/vnd.microsoft.card.hero",
         "content": {
           // other card fields here, see above
           // title: '...',
           // subtitle: '...',
           // ...
           "buttons": [
             {
               "type": "openUrl",
               "title": "Open Link",
               "value": "http://mywebsite.com"
             }
           ]
      }
    ]
  };

  bot.reply(messge, reply);
});

In addition to buttons, developers can add a "tap action" to a card which will be triggered when a user clicks on any part of the card - sort of like a default action.

Tap actions are defined using the same options as buttons - a card action object set in the message.tap field:

var attachment = {
    "contentType": "application/vnd.microsoft.card.hero",
    "content": {
      "title": "Hero card title",
      "subtitle": "Hero card subtitle",
      "text": "The text of my hero card"
      "images": [
        {
          "url": "http://placeimg.com/1600/900",
          "alt": "An image from placeimg.com"
        }
      ],
      "tap": {
        "type": "imBack",
        "value": "That tickles!"
      }
    }
  }

Botkit translates button clicks into invoke events. To respond to button click events, create one or more handlers for the invoke event.

The message object passed in with the invoke event has a value field which will match the value specified when the button was created.

Invoke events can be replied to, or used to start conversations, just like normal message events.

controller.on('invoke', function(bot, message) {

  // value is a user-defined object
  var value = message.value;

  // send a reply to the user
  bot.reply(messge,'You clicked a button!');

});

Your bot can @mention another user in a message, which causes their username to be highlighted and a special notification to be sent. Teams mentions are slightly more complex than some other platforms, and require not only a special syntax in the message itself, but also additional fields in the message object. See Microsoft's full docs for user mentions here.

A native Teams user mention requires BOTH:

This makes life a bit tricky, because the user name is not part of the incoming message, and requires additional API or DB calls to retrieve. Maintaining the entities field is also annoying!

To make life easier for developers, Botkit supports an easier to use syntax by providing a translation middleware. This allows developers to create mentions by including a modified mention syntax in the message text only, without having to also specify the entity field. Botkit uses a "Slack-style" mention syntax: <@USERID>.

Using this syntax, developers can create inline mentions in response to incoming messages with much less effort:

controller.hears('mention me', function(bot, message) {

  bot.reply(message,'I heard you, <@' + message.user +'>!');

});

One of the unique features of Microsoft Teams is support for "compose extensions" - custom UI elements that appear adjacent to the "compose" bar in the Teams client that allow users to create cards of their own using your bot application.

With a compose extension, you can offer users a way to search or create content in your application which is then attached to their message. Compose extensions can live in both multi-user team chats, as well as 1:1 discussions with the bot. They work sort of like web forms - as a user types a query, the compose extension API retrieves results from the application and displays them in the teams UI. When a result is selected, a custom app-defined card is attached to the user's outgoing message. Compose extensions use the same attachment format as normal messages.

To enable a compose extension in your bot app, you must first add a configuration section to your app's manifest file. Luckily, Botkit Studio has a tool for building these manifests. Using it will make your life much easier!

Once configured, whenever a user uses your compose extension, your Botkit application will receive a composeExtension event. Botkit automatically makes the user's query available in the message.text field, and provides a bot.replyToComposeExtension() function for formatting and delivering the results to Teams. replyToComposeExtension() expects the response to be an array of attachments.

controller.on('composeExtension', function(bot, message) {

  var query = message.text;

  my_custom_search(query).then(function(results) {

      // let's format the results an array of hero card attachments
      var attachments = [];
      for (var r = 0; r < results.length; r++) {
        var attachment = bot.createHero(results.title, results.subtitle, results.text);
        attachments.push(attachment);
      }

      // you can use the normal bot.reply function to send back the compose results!
      bot.replyToComposeExtension(message, results);

  });

});

Tab applications provide the ability to display web content directly in the Teams UI. There are a few different types of tab, and applications can contain multiple tabs. Microsoft has extensive documentation about building tab applications, but the short story is: your bot can include an integrated web app component that interacts with Teams in some neat ways. Microsoft provides an easy to use Javascript library that is used to set tab configuration options, and provide information about the user, team, and channels in which the tab is installed.

Tabs are configured in the manifest.json as part of your app package. Read up on that, or use Botkit Studio to build this file.

The Botkit Starter Kit for Microsoft Teams contains a complete tab application, and demonstrates the interplay between the tab and bot components. This is a great starting point, and gives you all pieces you'd otherwise have to build yourself.

The relevant information about building tabs in a Botkit application are:

When using the built-in webserver, developers can add web endpoints to the built in Express webserver inside the setupWebserver() callback function. Tabs serve normal webpages, and can live at whatever URI you specify - as long as it matches the settings in the manifest file.

controller.setupWebserver(3000, function(err, webserver) {
  // set up the normal webhooks to receive messages from teams
  controller.createWebhookEndpoints(webserver, function() {
      console.log("BOTKIT: Webhooks set up!");
  });

  // create a custom static tab url
  webserver.get('/static-tab', function(req, res) {
    res.send('This is my static tab url!')
  });

});

When using the starter kit, tab urls can be added in the components/routes/teams_tabs.js folder.

Using the Microsoft Teams Javascript library, developers can get access to the active user's "upn" - a user ID that identifies the user in the broader context of Microsoft Office 365.

This is a different user ID than the one used inside Teams chat . A little bit of extra work is necessary to connect the dots between these different account identifiers.

Botkit provides a method, bot.api.getUserByUpn(), that can be used to translate the upn value from the tab into user id expected by Teams chat. It is also possible to translate a message.user field into a upn by using bot.api.getUserById(), the results of which include the upn value.

For these reasons, we recommend using a user's upn value as the primary key when storing information about a user. The following example demonstrates using getUserById() to load a user's upn, then use the upn to load data from Botkit's built-in storage system. Using the storage system in this way will allow data stored in it to be used by both the bot and the tab application.

The starter kit implements this automatically using a middleware that automatically translates the user ID into a UPN and pre-loads the user data before firing any handlers. This is a good reference for any customized solution.

controller.hears('save', 'direct_message', function(bot, message) {

  var value = 'special value';
  // use the microsoft teams API to load user data
  bot.api.getUserById(message.channel, message.user, function(err, user_profile) {
    // check errors
    var upn = user_profile.userPrincipalName;
    controller.storage.users.get(upn, function(err, user_data) {
        // check errors

        // if no user found, create a new record with upn as primary id
        if (!user_data) {
          user_data = {
            id: upn
          }
        }

        // update with new value
        user_data.value = value;

        // store the user
        controller.storage.users.save(user_data);    

        // send a reply
        bot.reply(message,'Saved');
    });
  });
});

Argument Description
webserver An instance of the Express webserver

This function configures the route http://_your_server_/teams/receive to receive incoming event data from Microsoft Teams.

This url should be used when configuring your Bot Framework record.

Argument Description
options An object defining options for this specific bot instance - MUST include a serviceUrl.

This function returns a new instance of the bot. This is used internally by Botkit to respond to incoming events.

When spawning a bot for Microsoft Teams, you must pass in a serviceUrl field as part of the options parameter. The serviceUrl can be extracted from the incoming message payload at message.serviceUrl.

For those curious about this parameter: the serviceUrl is used to construct API calls the bot makes to Microsoft's API endpoints. The endpoint URLs are actually defined dynamically in response to different kinds of incoming messages. This is because Microsoft Teams is just one of a network of Microsoft products that uses the Bot Framework API specification, each one with its own endpoint URLs.

In the event that your bot needs to send outbound messages without first receiving an inbound event from teams, you should capture and store the serviceUrl value you receive from the bot_channel_join event, which indicates that a bot has been added to a new team.

var bot = controller.spawn({serviceUrl: my_team_info.serviceUrl});

Is something missing or out of date?

This file is managed on Github. click here to view the source, and send us a pull request with your improvements!