Back to Blog
6 min read

Microsoft Teams Integration with Azure: Building Collaborative Apps

Microsoft Teams has become the hub for workplace collaboration. At Ignite 2021, Microsoft announced enhanced capabilities for integrating custom applications with Teams using Azure services.

Teams Integration Options

Teams supports multiple integration patterns:

  • Tabs: Embed web apps in Teams
  • Bots: Conversational AI interfaces
  • Messaging Extensions: Rich interactions in chat
  • Webhooks: Push notifications to channels
  • Connectors: Pull data from external services

Building a Teams Bot with Azure Bot Service

Create Bot Registration

# Create Azure Bot
az bot create \
    --resource-group rg-teams-bot \
    --name my-teams-bot \
    --kind registration \
    --endpoint "https://mybot.azurewebsites.net/api/messages" \
    --sku F0 \
    --msi-resource-id "/subscriptions/.../identities/bot-identity"

# Enable Teams channel
az bot msteams create \
    --resource-group rg-teams-bot \
    --name my-teams-bot

Bot Implementation

// Bot/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 ILogger<TeamsBot> _logger;
    private readonly ITicketService _ticketService;

    public TeamsBot(ILogger<TeamsBot> logger, ITicketService ticketService)
    {
        _logger = logger;
        _ticketService = ticketService;
    }

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

        if (text?.StartsWith("create ticket") == true)
        {
            await HandleCreateTicketAsync(turnContext, text, cancellationToken);
        }
        else if (text?.StartsWith("status") == true)
        {
            await HandleStatusQueryAsync(turnContext, cancellationToken);
        }
        else
        {
            // Show help card
            var card = CreateHelpCard();
            await turnContext.SendActivityAsync(
                MessageFactory.Attachment(card),
                cancellationToken
            );
        }
    }

    private async Task HandleCreateTicketAsync(
        ITurnContext<IMessageActivity> turnContext,
        string text,
        CancellationToken cancellationToken)
    {
        var description = text.Replace("create ticket", "").Trim();

        var ticket = await _ticketService.CreateTicketAsync(new CreateTicketRequest
        {
            Description = description,
            ReportedBy = turnContext.Activity.From.Name,
            Channel = turnContext.Activity.ChannelId
        });

        var card = CreateTicketCard(ticket);
        await turnContext.SendActivityAsync(
            MessageFactory.Attachment(card),
            cancellationToken
        );
    }

    protected override async Task OnTeamsChannelCreatedAsync(
        ChannelInfo channelInfo,
        TeamInfo teamInfo,
        ITurnContext<IConversationUpdateActivity> turnContext,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Channel created: {ChannelName} in team {TeamName}",
            channelInfo.Name, teamInfo.Name);

        await turnContext.SendActivityAsync(
            $"Welcome to {channelInfo.Name}! I'm here to help manage support tickets.",
            cancellationToken: cancellationToken
        );
    }

    protected override async Task OnMembersAddedAsync(
        IList<ChannelAccount> membersAdded,
        ITurnContext<IConversationUpdateActivity> turnContext,
        CancellationToken cancellationToken)
    {
        foreach (var member in membersAdded)
        {
            if (member.Id != turnContext.Activity.Recipient.Id)
            {
                var welcomeCard = CreateWelcomeCard(member.Name);
                await turnContext.SendActivityAsync(
                    MessageFactory.Attachment(welcomeCard),
                    cancellationToken
                );
            }
        }
    }

    private Attachment CreateTicketCard(Ticket ticket)
    {
        var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 3))
        {
            Body = new List<AdaptiveElement>
            {
                new AdaptiveTextBlock
                {
                    Text = $"Ticket #{ticket.Id}",
                    Weight = AdaptiveTextWeight.Bolder,
                    Size = AdaptiveTextSize.Large
                },
                new AdaptiveFactSet
                {
                    Facts = new List<AdaptiveFact>
                    {
                        new AdaptiveFact("Status", ticket.Status),
                        new AdaptiveFact("Priority", ticket.Priority),
                        new AdaptiveFact("Reported By", ticket.ReportedBy),
                        new AdaptiveFact("Created", ticket.CreatedAt.ToString("g"))
                    }
                },
                new AdaptiveTextBlock
                {
                    Text = ticket.Description,
                    Wrap = true
                }
            },
            Actions = new List<AdaptiveAction>
            {
                new AdaptiveSubmitAction
                {
                    Title = "Update Status",
                    Data = new { action = "updateStatus", ticketId = ticket.Id }
                },
                new AdaptiveOpenUrlAction
                {
                    Title = "View Details",
                    Url = new Uri($"https://ticketsystem.com/tickets/{ticket.Id}")
                }
            }
        };

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

Handle Adaptive Card Actions

protected override async Task<InvokeResponse> OnAdaptiveCardInvokeAsync(
    ITurnContext<IInvokeActivity> turnContext,
    AdaptiveCardInvokeValue invokeValue,
    CancellationToken cancellationToken)
{
    var action = invokeValue.Action.Data.ToString();
    var data = JsonSerializer.Deserialize<CardActionData>(action);

    switch (data.Action)
    {
        case "updateStatus":
            return await HandleUpdateStatusAsync(turnContext, data, cancellationToken);

        case "assignTicket":
            return await HandleAssignTicketAsync(turnContext, data, cancellationToken);

        default:
            return CreateInvokeResponse(HttpStatusCode.BadRequest);
    }
}

private async Task<InvokeResponse> HandleUpdateStatusAsync(
    ITurnContext<IInvokeActivity> turnContext,
    CardActionData data,
    CancellationToken cancellationToken)
{
    var ticket = await _ticketService.UpdateStatusAsync(data.TicketId, data.NewStatus);

    // Update the card in place
    var updatedCard = CreateTicketCard(ticket);

    return CreateInvokeResponse(HttpStatusCode.OK, new AdaptiveCardInvokeResponse
    {
        StatusCode = 200,
        Type = AdaptiveCard.ContentType,
        Value = updatedCard.Content
    });
}

Messaging Extensions

Search Extension

protected override async Task<MessagingExtensionResponse> OnTeamsMessagingExtensionQueryAsync(
    ITurnContext<IInvokeActivity> turnContext,
    MessagingExtensionQuery query,
    CancellationToken cancellationToken)
{
    var searchText = query.Parameters?.FirstOrDefault(p => p.Name == "searchText")?.Value?.ToString();

    var tickets = await _ticketService.SearchTicketsAsync(searchText);

    var attachments = tickets.Select(ticket => new MessagingExtensionAttachment
    {
        ContentType = HeroCard.ContentType,
        Content = new HeroCard
        {
            Title = $"Ticket #{ticket.Id}",
            Subtitle = ticket.Status,
            Text = ticket.Description
        },
        Preview = new HeroCard
        {
            Title = $"Ticket #{ticket.Id}",
            Subtitle = ticket.Status,
            Tap = new CardAction
            {
                Type = ActionTypes.InvokeAction,
                Value = new { ticketId = ticket.Id }
            }
        }.ToAttachment()
    }).ToList();

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

Action Extension

protected override async Task<MessagingExtensionActionResponse> OnTeamsMessagingExtensionSubmitActionAsync(
    ITurnContext<IInvokeActivity> turnContext,
    MessagingExtensionAction action,
    CancellationToken cancellationToken)
{
    var data = JsonSerializer.Deserialize<CreateTicketFormData>(action.Data.ToString());

    var ticket = await _ticketService.CreateTicketAsync(new CreateTicketRequest
    {
        Title = data.Title,
        Description = data.Description,
        Priority = data.Priority,
        ReportedBy = turnContext.Activity.From.Name
    });

    var card = CreateTicketCard(ticket);

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

Incoming Webhooks

Send to Teams Channel

public class TeamsNotificationService
{
    private readonly HttpClient _httpClient;

    public TeamsNotificationService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task SendAlertAsync(string webhookUrl, Alert alert)
    {
        var card = new
        {
            type = "message",
            attachments = new[]
            {
                new
                {
                    contentType = "application/vnd.microsoft.card.adaptive",
                    content = new
                    {
                        type = "AdaptiveCard",
                        version = "1.3",
                        body = new object[]
                        {
                            new
                            {
                                type = "TextBlock",
                                text = alert.Severity == "Critical" ? "!!! Critical Alert" : "Alert",
                                weight = "Bolder",
                                size = "Large",
                                color = alert.Severity == "Critical" ? "Attention" : "Default"
                            },
                            new
                            {
                                type = "TextBlock",
                                text = alert.Title,
                                weight = "Bolder"
                            },
                            new
                            {
                                type = "FactSet",
                                facts = new[]
                                {
                                    new { title = "Service", value = alert.Service },
                                    new { title = "Environment", value = alert.Environment },
                                    new { title = "Time", value = alert.Timestamp.ToString("g") }
                                }
                            },
                            new
                            {
                                type = "TextBlock",
                                text = alert.Description,
                                wrap = true
                            }
                        },
                        actions = new[]
                        {
                            new
                            {
                                type = "Action.OpenUrl",
                                title = "View in Portal",
                                url = alert.PortalUrl
                            }
                        }
                    }
                }
            }
        };

        var content = new StringContent(
            JsonSerializer.Serialize(card),
            Encoding.UTF8,
            "application/json"
        );

        await _httpClient.PostAsync(webhookUrl, content);
    }
}

Tab Application

Teams Tab Configuration

// tabs/config.tsx
import * as microsoftTeams from "@microsoft/teams-js";

export function ConfigPage() {
    const [selectedDashboard, setSelectedDashboard] = useState("");

    useEffect(() => {
        microsoftTeams.initialize();

        microsoftTeams.settings.registerOnSaveHandler((saveEvent) => {
            microsoftTeams.settings.setSettings({
                websiteUrl: `https://myapp.com/dashboard/${selectedDashboard}`,
                contentUrl: `https://myapp.com/dashboard/${selectedDashboard}?inTeams=true`,
                entityId: selectedDashboard,
                suggestedDisplayName: `Dashboard - ${selectedDashboard}`
            });
            saveEvent.notifySuccess();
        });
    }, [selectedDashboard]);

    useEffect(() => {
        microsoftTeams.settings.setValidityState(selectedDashboard !== "");
    }, [selectedDashboard]);

    return (
        <div>
            <h2>Select Dashboard</h2>
            <select
                value={selectedDashboard}
                onChange={(e) => setSelectedDashboard(e.target.value)}
            >
                <option value="">Select...</option>
                <option value="sales">Sales Dashboard</option>
                <option value="support">Support Dashboard</option>
                <option value="devops">DevOps Dashboard</option>
            </select>
        </div>
    );
}

Teams Context in Tab

// tabs/dashboard.tsx
import * as microsoftTeams from "@microsoft/teams-js";

export function Dashboard() {
    const [context, setContext] = useState<microsoftTeams.Context | null>(null);
    const [data, setData] = useState(null);

    useEffect(() => {
        microsoftTeams.initialize();

        microsoftTeams.getContext((ctx) => {
            setContext(ctx);
            loadDashboardData(ctx.entityId);
        });

        // Handle theme changes
        microsoftTeams.registerOnThemeChangeHandler((theme) => {
            document.body.className = theme;
        });
    }, []);

    async function loadDashboardData(dashboardId: string) {
        // Use Teams SSO for authentication
        const token = await getTeamsToken();

        const response = await fetch(`/api/dashboard/${dashboardId}`, {
            headers: { Authorization: `Bearer ${token}` }
        });

        setData(await response.json());
    }

    async function getTeamsToken(): Promise<string> {
        return new Promise((resolve, reject) => {
            microsoftTeams.authentication.getAuthToken({
                successCallback: resolve,
                failureCallback: reject
            });
        });
    }

    if (!data) return <div>Loading...</div>;

    return (
        <div className={`dashboard ${context?.theme}`}>
            <h1>Dashboard for {context?.teamName}</h1>
            {/* Dashboard content */}
        </div>
    );
}

Microsoft Teams integration with Azure enables powerful collaborative applications. Whether building bots, tabs, or webhooks, the combination provides a seamless experience for users.

Resources

Michael John Pena

Michael John Pena

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