Back to Blog
6 min read

Building Teams Apps: Integration Patterns and Best Practices

Microsoft Teams has become the hub for workplace collaboration. Building apps for Teams allows you to bring your services directly to where users work. Build 2022 showcased new capabilities for Teams developers.

Teams App Components

Teams apps can include:

  • Tabs (embedded web content)
  • Bots (conversational interfaces)
  • Message extensions
  • Webhooks and connectors
  • Meeting extensions

Setting Up Teams Toolkit

# Install Teams Toolkit CLI
npm install -g @microsoft/teamsfx-cli

# Create new Teams app
teamsfx new --interactive false --capability tab --programming-language typescript

# Or create with bot capability
teamsfx new --interactive false --capability bot --programming-language csharp

Tab Application

Create an embedded web application:

// src/components/Tab.tsx
import React, { useEffect, useState } from "react";
import { app, authentication } from "@microsoft/teams-js";
import { Loader, Text, Flex, Card } from "@fluentui/react-northstar";

interface UserProfile {
  displayName: string;
  email: string;
  jobTitle: string;
}

export const Tab: React.FC = () => {
  const [loading, setLoading] = useState(true);
  const [profile, setProfile] = useState<UserProfile | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const initializeTeams = async () => {
      try {
        await app.initialize();

        // Get authentication token
        const token = await authentication.getAuthToken({
          resources: ["https://graph.microsoft.com"],
        });

        // Fetch user profile
        const response = await fetch("https://graph.microsoft.com/v1.0/me", {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        });

        if (response.ok) {
          const data = await response.json();
          setProfile({
            displayName: data.displayName,
            email: data.mail || data.userPrincipalName,
            jobTitle: data.jobTitle || "Not specified",
          });
        }
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to initialize");
      } finally {
        setLoading(false);
      }
    };

    initializeTeams();
  }, []);

  if (loading) {
    return <Loader label="Loading..." />;
  }

  if (error) {
    return <Text error content={error} />;
  }

  return (
    <Flex column gap="gap.medium" padding="padding.medium">
      <Text size="larger" weight="bold" content="Welcome to the App" />
      {profile && (
        <Card>
          <Card.Header>
            <Text content={profile.displayName} weight="bold" />
          </Card.Header>
          <Card.Body>
            <Flex column gap="gap.small">
              <Text content={`Email: ${profile.email}`} />
              <Text content={`Title: ${profile.jobTitle}`} />
            </Flex>
          </Card.Body>
        </Card>
      )}
    </Flex>
  );
};

Bot Implementation

Create a conversational bot:

// Bots/TeamsBot.cs
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Schema.Teams;

public class TeamsBot : TeamsActivityHandler
{
    private readonly IConfiguration _config;
    private readonly ILogger<TeamsBot> _logger;

    public TeamsBot(IConfiguration config, ILogger<TeamsBot> logger)
    {
        _config = config;
        _logger = logger;
    }

    protected override async Task OnMessageActivityAsync(
        ITurnContext<IMessageActivity> turnContext,
        CancellationToken cancellationToken)
    {
        var text = turnContext.Activity.Text?.Trim().ToLower();

        if (string.IsNullOrEmpty(text))
        {
            await turnContext.SendActivityAsync("Please send a message.");
            return;
        }

        // Handle different commands
        var response = text switch
        {
            "help" => CreateHelpCard(),
            "status" => await GetStatusCard(turnContext),
            _ when text.StartsWith("search ") => await SearchAsync(text[7..], turnContext),
            _ => MessageFactory.Text($"You said: {text}")
        };

        await turnContext.SendActivityAsync(response, cancellationToken);
    }

    private IMessageActivity CreateHelpCard()
    {
        var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 4))
        {
            Body = new List<AdaptiveElement>
            {
                new AdaptiveTextBlock
                {
                    Text = "Available Commands",
                    Weight = AdaptiveTextWeight.Bolder,
                    Size = AdaptiveTextSize.Large
                },
                new AdaptiveFactSet
                {
                    Facts = new List<AdaptiveFact>
                    {
                        new("help", "Show this help message"),
                        new("status", "Check system status"),
                        new("search <query>", "Search for items")
                    }
                }
            }
        };

        var attachment = new Attachment
        {
            ContentType = AdaptiveCard.ContentType,
            Content = card
        };

        return MessageFactory.Attachment(attachment);
    }

    private async Task<IMessageActivity> GetStatusCard(ITurnContext context)
    {
        // Fetch status from your API
        var status = new
        {
            Api = "Healthy",
            Database = "Healthy",
            LastUpdated = DateTime.UtcNow
        };

        var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 4))
        {
            Body = new List<AdaptiveElement>
            {
                new AdaptiveTextBlock
                {
                    Text = "System Status",
                    Weight = AdaptiveTextWeight.Bolder
                },
                new AdaptiveColumnSet
                {
                    Columns = new List<AdaptiveColumn>
                    {
                        new AdaptiveColumn
                        {
                            Items = new List<AdaptiveElement>
                            {
                                new AdaptiveTextBlock { Text = "API" },
                                new AdaptiveTextBlock { Text = "Database" }
                            }
                        },
                        new AdaptiveColumn
                        {
                            Items = new List<AdaptiveElement>
                            {
                                new AdaptiveTextBlock
                                {
                                    Text = status.Api,
                                    Color = AdaptiveTextColor.Good
                                },
                                new AdaptiveTextBlock
                                {
                                    Text = status.Database,
                                    Color = AdaptiveTextColor.Good
                                }
                            }
                        }
                    }
                }
            }
        };

        return MessageFactory.Attachment(new Attachment
        {
            ContentType = AdaptiveCard.ContentType,
            Content = card
        });
    }

    private async Task<IMessageActivity> SearchAsync(string query, ITurnContext context)
    {
        // Implement search logic
        return MessageFactory.Text($"Searching for: {query}");
    }

    protected override async Task OnTeamsMembersAddedAsync(
        IList<TeamsChannelAccount> membersAdded,
        TeamInfo teamInfo,
        ITurnContext<IConversationUpdateActivity> turnContext,
        CancellationToken cancellationToken)
    {
        foreach (var member in membersAdded)
        {
            if (member.Id != turnContext.Activity.Recipient.Id)
            {
                await turnContext.SendActivityAsync(
                    $"Welcome {member.Name}! Type 'help' to see available commands.",
                    cancellationToken: cancellationToken);
            }
        }
    }
}

Message Extension

Create a search-based message extension:

// Bots/MessageExtension.cs
public class MessageExtension : TeamsActivityHandler
{
    protected override async Task<MessagingExtensionResponse> OnTeamsMessagingExtensionQueryAsync(
        ITurnContext<IInvokeActivity> turnContext,
        MessagingExtensionQuery query,
        CancellationToken cancellationToken)
    {
        var searchText = query.Parameters?.FirstOrDefault(p => p.Name == "search")?.Value as string ?? "";

        // Search your data source
        var results = await SearchItemsAsync(searchText);

        var attachments = results.Select(item => new MessagingExtensionAttachment
        {
            ContentType = HeroCard.ContentType,
            Content = new HeroCard
            {
                Title = item.Title,
                Subtitle = item.Description,
                Images = new List<CardImage>
                {
                    new CardImage(item.ImageUrl)
                }
            },
            Preview = new HeroCard
            {
                Title = item.Title,
                Tap = new CardAction
                {
                    Type = ActionTypes.InvokeAction,
                    Value = item
                }
            }.ToAttachment()
        }).ToList();

        return new MessagingExtensionResponse
        {
            ComposeExtension = new MessagingExtensionResult
            {
                Type = "result",
                AttachmentLayout = "list",
                Attachments = attachments
            }
        };
    }

    protected override async Task<MessagingExtensionActionResponse> OnTeamsMessagingExtensionSubmitActionAsync(
        ITurnContext<IInvokeActivity> turnContext,
        MessagingExtensionAction action,
        CancellationToken cancellationToken)
    {
        var data = action.Data as JObject;
        var title = data?["title"]?.ToString();
        var description = data?["description"]?.ToString();

        var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 4))
        {
            Body = new List<AdaptiveElement>
            {
                new AdaptiveTextBlock
                {
                    Text = title,
                    Weight = AdaptiveTextWeight.Bolder,
                    Size = AdaptiveTextSize.Large
                },
                new AdaptiveTextBlock
                {
                    Text = description,
                    Wrap = true
                }
            }
        };

        return new MessagingExtensionActionResponse
        {
            ComposeExtension = new MessagingExtensionResult
            {
                Type = "result",
                AttachmentLayout = "list",
                Attachments = new List<MessagingExtensionAttachment>
                {
                    new MessagingExtensionAttachment
                    {
                        ContentType = AdaptiveCard.ContentType,
                        Content = card
                    }
                }
            }
        };
    }

    private async Task<List<SearchItem>> SearchItemsAsync(string query)
    {
        // Implement your search logic
        return new List<SearchItem>
        {
            new("Item 1", "Description for item 1", "https://example.com/image1.png"),
            new("Item 2", "Description for item 2", "https://example.com/image2.png")
        };
    }
}

public record SearchItem(string Title, string Description, string ImageUrl);

App Manifest

Configure your Teams app:

{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.14/MicrosoftTeams.schema.json",
  "manifestVersion": "1.14",
  "version": "1.0.0",
  "id": "{{APP_ID}}",
  "packageName": "com.example.teamsapp",
  "developer": {
    "name": "Contoso",
    "websiteUrl": "https://example.com",
    "privacyUrl": "https://example.com/privacy",
    "termsOfUseUrl": "https://example.com/terms"
  },
  "name": {
    "short": "My Teams App",
    "full": "My Teams App - Full Name"
  },
  "description": {
    "short": "Short description",
    "full": "Full description of the app"
  },
  "icons": {
    "color": "color.png",
    "outline": "outline.png"
  },
  "accentColor": "#FFFFFF",
  "staticTabs": [
    {
      "entityId": "home",
      "name": "Home",
      "contentUrl": "https://{{HOSTNAME}}/tab",
      "scopes": ["personal"]
    }
  ],
  "bots": [
    {
      "botId": "{{BOT_ID}}",
      "scopes": ["personal", "team", "groupchat"],
      "commandLists": [
        {
          "scopes": ["personal"],
          "commands": [
            {
              "title": "help",
              "description": "Show help"
            },
            {
              "title": "status",
              "description": "Check status"
            }
          ]
        }
      ]
    }
  ],
  "composeExtensions": [
    {
      "botId": "{{BOT_ID}}",
      "commands": [
        {
          "id": "searchQuery",
          "type": "query",
          "title": "Search",
          "description": "Search for items",
          "parameters": [
            {
              "name": "search",
              "title": "Search",
              "description": "Enter search terms"
            }
          ]
        }
      ]
    }
  ],
  "permissions": ["identity", "messageTeamMembers"],
  "validDomains": ["{{HOSTNAME}}"]
}

Summary

Teams app development offers:

  • Multiple extension points (tabs, bots, extensions)
  • Rich adaptive card experiences
  • Deep integration with Microsoft 365
  • SSO with Azure AD
  • Comprehensive SDK support

Build collaborative experiences that meet users where they work.


References:

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.