ODC with Loop Components - Basic Setup and Link Unfurling

Stefan WeberStefan Weber
17 min read

In this first tutorial of the ODC with Loop Components series, we will set up our environment by creating an Application registration in Entra and setting up an Azure Bot resource. Then, we will explore the implementation details needed to turn an application URL into an Adaptive Card-based Loop Component, known as Link unfurling. This involves:

  • App Manifest - The App Manifest is a configuration file that defines our Microsoft 365 app. It specifically configures settings for link unfurling. The App Manifest is packaged with icon resources and then uploaded to Microsoft 365 through the Microsoft Teams app.

  • Messaging Endpoint - An exposed REST API in ODC that receives link unfurling requests - via the Azure Bot resource -, processes them, and returns the Adaptive Card-based Loop Component.

Demo Application

This article series includes a demo application called "ODC with Loop Components Demo," available on ODC Forge. Be sure to download the version of the application that matches each article in this series.

For this article, you need to install Version 0.1 from ODC Forge.

  • In the ODC Portal, go to Forge - All Assets.

  • Search for "ODC with Loop Components Demo".

  • Click on the Asset (Do not click on Install on the tile!).

  • Switch to the Version tab and click on Install next to Version 0.1.

Version 0.1 depends on other Forge components:

  • OAuthTokenExchange - An external logic library that helps retrieve access tokens easily. In this tutorial however we use an action to retrieve the discovery document from Microsoft Entra only.

  • LiquidFluid - An external logic library that merges data with templates based on the Shopify Liquid Templating language.

  • UriParser - An external logic library that parses a given URI/URL.

In the demo, you can create and edit auction entries with a title, description, and starting auction price. In Version 0.1, this is the only feature available, but we will expand it throughout the tutorial series.

After this tutorial, we want to be able to paste a link from an auction detail screen into a Teams conversation. This link should expand into an Adaptive Card-based Loop component that looks like this:

In later parts of the series, we will enable interaction with this card, allowing users to bid on the item.

Messaging Endpoint

The Building a Loop Component section in the series' introductory article mentiones that we need to create a Messaging Endpoint REST API to handle link unfurling. If you've read my ODC with Bots for Teams series, you might wonder why we aren't reusing what we built there and why this is a separate tutorial series.

In the ODC with Bots for Teams series, we developed a pattern for Conversational Bots that you can interact with in Microsoft Teams or other chat platforms like Alexa and Slack.

Conversational Bots are one feature that can be configured in an App Manifest and then uploaded to your Microsoft 365 environment.

Link unfurling, which transforms a link into an Adaptive Card-based Loop component, is another feature known as a Message Extension.

Conversational Bots and Message Extensions use the same core technology stack. This includes a Bot resource in Azure with an associated Entra application registration and an exposed REST API in ODC, which serves as the Messaging Endpoint handling requests from either a Conversational Bot channel or a Message Extension added to a Microsoft 365 app.

However, there is a fundamental difference in how Bots and Message Extensions send a response to a channel.

  • Conversational Bots - Conversational Bots - the Bot handlers to be more precise - send a response to the channel via the Azure Bot Connector API.

  • Message Extensions - For Message Extensions, the response is directly returned from the Messaging Endpoint REST API.

💡
Don't be confused by the term Bots. Both types need an Azure Bot resource.

Conversational Bots

In ODC, we can use one Messaging Endpoint to manage requests from multiple Azure Bot resources and configured channels. The ODC with Bots for Teams series shows how to create this type of multi-bot handler using ODC events. The diagram below illustrates this pattern.

In this diagram, multiple Azure Bot resources use a single Messaging Endpoint REST API within a central ODC application. The Messaging Endpoint authorizes the Bot, stores the incoming activity in an entity, and then triggers an ODC event with details of the incoming activity.

Several other ODC applications can subscribe to this event, process the activity, and then respond to the channel using the Azure Bot Connector API.

Message Extensions

For Message Extensions, we can't use this decoupled pattern because the Messaging Endpoint must respond directly to the channel. Additionally, a single domain, such as your ODC development stage company-dev.outsystems.app, can only be managed by one - and only one - message extension, and there's no option to include a path. A message extension is defined in the App Manifest. Take a look at the following diagram:

This diagram shows a setup for handling link unfurling and Adaptive Card-based Loop Components.

A single Azure Bot resource with a configured messaging endpoint can manage link unfurling and Adaptive Card actions because the specific domains are listed in the App Manifest. You can create multiple Message Extension App Manifests for one bot resource.

The Messaging Endpoint that receives requests can be set up in a central ODC application with logic to differentiate between the relevant domains based on the domains and even path segments. It then uses service actions from other ODC applications to create or update an Adaptive Card or handle an Adaptive Card action.

While this approach isn't perfect, I used it because I couldn't find a better option.

💡
Note that the tutorial demo application doesn't use this pattern but instead has all logic contained in a single web app. The pattern above is meant to give you an idea of how to implement Message Extensions in a production environment.

Prerequisites

Enough with the theory. Let's start by preparing our environment.

Register Entra Application

A bot resource requires an Entra application registration.

In Azure Portal go to App Registrations and click on New registration.

  • Name - Microsoft 365 Message Extension with ODC

  • Supported account types - Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)

  • Click Register

From the Overview page, copy the values of:

  • Application (client) ID

We will need that value in the next step.

Create an Azure Bot

With our application registration complete, we can now create an Azure Bot resource. An Azure Bot resource is a middleware that connects Microsoft 365 apps (and others) with the Messaging Endpoint in ODC.

In Azure Marketplace search for Azure Bot and click Create - Azure Bot.

  • Bot handle - BotId-<Application ID from Entra App Registration>

  • Subscription - Select your subscription model

  • Resource group - Choose a resource group where you want to place the bot resource

  • Data residency - Global

  • Pricing tier - Change plan to Free

  • Type of App - Multi Tenant (corresponds to our Entra App Registration)

  • Creation type - Use existing app registration

  • App ID - Paste the Application ID from the Entra App Registration Overview page

  • App tenant ID - Paste the Tenant ID from the Entra App Registration Overview page

  • Click Review + Create, review the information the click Create

Wait until the deployment is complete, then click on Go to resource to continue.

Set Bot Profile

In the deployed Azure Bot resource, first go to the Settings - Bot profile menu and give your bot a meaningful display name and description. You can also upload a custom icon here.

Configure Messaging Endpoint

Next switch to the Settings - Configuration menu. The only settings we have to configure here is to provide the FQDN to our Messaging endpoint.

The messaging endpoint FQDN is the combination of your ODC stage base URL and the path you copied earlier from the Messaged endpoint of the demo application.

Activate Teams Channel

Next, we need to activate Teams channel support in our Bot resource.

  • Choose Microsoft Teams from the list of Available Channels in the Settings - Channels menu.

  • Read the Terms of Service and Agree.

  • In the Messaging tab, select Microsoft Teams Commercial (most common).

  • Click Apply.

Summary

In the Prerequisites step, we created an Azure Bot resource with an associated Entra application registration. In the Bot configuration, we specified our Messaging Endpoint hosted in the ODC with Loop Components Demo application. Finally, we activated the Microsoft Teams channel.

As mentioned above, an Azure Bot resource in your Azure tenant acts as middleware. This middleware exchanges messages, called Activities, between configured channels and the messaging endpoint. It does not matter if you are building Conversational Bots or Message Extensions. These capabilities, or what your implementation actually does, are specified in the App Manifest.

Microsoft 365 App

The App Manifest document describes and configures the capabilities that this specific app—your implementation—can perform. In the ODC with Bots for Teams series, we covered the Conversational Bot capability, and in this tutorial, we configure a Message Extension capability for Link unfurling.

The App Manifest, along with additional resources like icons, needs to be uploaded to your Microsoft 365 tenant through either Microsoft Teams or the Microsoft Office Admin Portal. We will use Microsoft Teams and will "install" the app for a single user, a process known as sideloading.

Create App Manifest

The demo project includes a template for the manifest file, complete with icons. Download the template archive from Data - Resources in ODC Studio and extract it to a folder.

An app manifest file is a JSON document that follows the unified app manifest schema. Together with the manifest file you will also need two icons.

  • Full color icon with a size of 192×192

  • Outline icon with a size of 32×32

Open the manifest.json document from the extracted template in an editor of your choice.

{
    "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json",
    "version": "3.0.0",
    "manifestVersion": "1.19",
    "id": "<Entra Application (client) ID>",
    "name": {
      "short": "ODCwithLoop",
      "full": "ODCwithLoop Demo"
    },
    "developer": {
      "name": "without.systems",
      "websiteUrl": "https://without.systems",
      "privacyUrl": "https://without.systems/privacy",
      "termsOfUseUrl": "https://without.systems/termsofuse"
    },
    "description": {
      "short": "Shows how to build a Loop Component Adaptive Card",
      "full": "Create interaktive elements with Adaptive Cards and share it across Microsoft 365 apps."
    },
    "icons": {
      "outline": "outline.png",
      "color": "color.png"
    },
    "accentColor": "#FFFFFF",
    "composeExtensions": [
      {
        "botId": "<Entra Application (client) ID>",
        "composeExtensionType": "botBased",
        "commands": [],
        "messageHandlers": [
          {
            "type": "link",
            "value": {
              "domains": [
                "<your ODC development stage domain e.g. company-dev.outsystems.app>"
              ],
              "supportsAnonymizedPayloads": false
            }
          }
        ]
      }
    ],
    "permissions": [
      "identity",
      "messageTeamMembers"
    ],
    "validDomains": [
      "<your ODC development stage domain e.g. company-dev.outsystems.app>"
    ]
  }

Modify the following values to match your environment

  • id - Application (client) ID from your Entra Application registration

  • composeExtensions.botId - Application (client) ID from your Entra Application registration

  • composeExtensions.messageHandlers.value.domains - Add your ODC development stage domain name.

  • validDomains - Add your ODC development stage domain name.

Save the document, then compress the manifest.json file and the two icon resources into a ZIP archive.

Allow Sideloading

By default users are not allowed to sideload custom apps into Microsoft Teams. This is defined in a Teams Setup Policy that is associated with your user account in the Microsoft Teams Admin center.

In the Microsoft Teams Admin center.

  • Select Teams apps - Setup policies in the menu

  • Click Add

  • Name: OutSystems Bot Developer Setup Policy

  • Description: Allows an OutSystems Bot developer to sideload Microsoft Teams apps

  • Click Save

After we have created the policy, we must assign it to one or more users

  • Select Users - Manage users in the menu

  • Search your own user account and select it

  • In the Policies tab click Edit

  • Select the OutSystems Bot Developer Setup Policy in the Select App setup policy dropdown

  • Click Apply

💡
It may take a little while for the new settings to take effect. You will need to restart your Microsoft Teams client.

Install App to Microsoft Teams

With our package prepared we can now install it to Microsoft Teams. Open your Microsoft Teams client and in the left icon menu click the Apps icon.

  • On the bottom left click on Manage your apps

  • In the apps list click the Upload an app button and select Upload a custom app

  • Select the zip archive you created

  • Check the details of the app, then click Add

With our app uploaded to Microsoft Teams, we can now proceed to try out the demo application.

Demo Application - First Try

Open the Demo application in your browser and create a new Auction. After saving, click on the entry of the created Auction again, which will take you back to the detail screen. Copy the full URL from the browser.

Open Microsoft Teams and start a chat conversation with another test account - or a collague you want to bother with your tests 😒.

Paste the URL into the message box, and after a short while, a Loop component should appear next to the pasted URL in the message box.

If it didn't work, check the following:

  • Review your app manifest to ensure you added the correct domain for link unfurling.

  • Add a breakpoint at the top of the Messages endpoint in the demo application, start the debugger, and check if a request is sent to your REST API.

  • Verify the messaging endpoint URL in the Bot resource in Azure to ensure it matches the correct full endpoint URL.

  • Check the botId value to ensure it matches your Entra Application (client) Id.

Before we dive into the implementation details, let's cover one more important part: Adaptive Cards.

Adaptive Cards

When you copy an auction link into a Teams message, it expands into a Loop component based on an Adaptive Card. These are basically JSON-based UI snippets that the host application displays. Host applications are mainly Microsoft 365 apps, but there is also a JavaScript-based SDK to display Adaptive Cards in any web application.

The template for the Adaptive Card JSON document for our Auction looks like this:

{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.4",
    "type": "AdaptiveCard",
    "metadata": {
        "webUrl": "{{webUrl}}"
    },
    "body": [
        {
            "type": "TextBlock",
            "size": "medium",
            "weight": "bolder",
            "text": "{{title}}"
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "{{description}}",
                            "wrap": true
                        }
                    ],
                    "verticalContentAlignment": "Center",
                    "spacing": "Medium"
                },
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "{{price}}",
                            "wrap": true,
                            "size": "ExtraLarge",
                            "weight": "Bolder"
                        }
                    ]
                }
            ],
            "spacing": "None"
        }
    ]
}

This card defines one Text Block containing the title of the auction followed by a table with two columns containing the description and the price.

Note the handlebar placeholders like {{title}} that will be replaced with values in our implementation as we will see in a bit.

The full reference documentation for Adaptive Cards along with samples can be found at Welcome - Adaptive Cards.

You can create Adaptive Cards either using a text editor or visually using the Adapative Card Designer.

Adaptive Cards in a Message Extension

To expand a link into an Adaptive Card, the Messaging Endpoint must return it in a specific format that looks like this:

{
    "composeExtension": {
        "type": "result",
        "attachmentLayout": "list",
        "attachments": [
            {
                "contentType": "application/vnd.microsoft.card.adaptive",
                "preview": {
                    "contentType": "application/vnd.microsoft.card.adaptive",
                    "content": {
                        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                        "version": "1.4",
                        "type": "AdaptiveCard",
                        "metadata": {
                            "webUrl": "{{webUrl}}"
                        },
                        "body": [
                            {
                                "type": "TextBlock",
                                "size": "medium",
                                "weight": "bolder",
                                "text": "{{title}}"
                            }
                        ]
                    }
                },
                "content": {
                    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                    "version": "1.4",
                    "type": "AdaptiveCard",
                    "metadata": {
                        "webUrl": "{{webUrl}}"
                    },
                    "body": [
                        {
                            "type": "TextBlock",
                            "size": "medium",
                            "weight": "bolder",
                            "text": "{{title}}"
                        },
                        {
                            "type": "ColumnSet",
                            "columns": [
                                {
                                    "type": "Column",
                                    "width": "stretch",
                                    "items": [
                                        {
                                            "type": "TextBlock",
                                            "text": "{{description}}",
                                            "wrap": true
                                        }
                                    ],
                                    "verticalContentAlignment": "Center",
                                    "spacing": "Medium"
                                },
                                {
                                    "type": "Column",
                                    "width": "auto",
                                    "items": [
                                        {
                                            "type": "TextBlock",
                                            "text": "{{price}}",
                                            "wrap": true,
                                            "size": "ExtraLarge",
                                            "weight": "Bolder"
                                        }
                                    ]
                                }
                            ],
                            "spacing": "None"
                        }
                    ]
                }
            }
        ]
    }
}

The Messaging Endpoint returns a result of composeExtension for a Link unfurling request, which includes an Attachment. This attachment has two important properties: preview and content. Both are needed for Link unfurling to work. The preview should contain a simplified version of the Adaptive Card, while the content should have the full version. In the example above, the preview only includes the auction title.

Implementation Walkthrough

Now, let's look at the different implementation details that will turn an auction link into a Loop component. The steps in this first tutorial are straightforward:

  • Handle the queryLink request - Whenever a user pastes a link that matches the domain in the app manifest into a Teams message, the Teams channel sends a queryLink request to the configured Azure Bot resource, which then forwards the request to the Messaging endpoint.

  • Unfurl - In our Messaging endpoint handler, we will process the request, look up the auction in the database, and finally return a response containing the Adaptive Card.

Open the ODC with Loop Components Demo application in ODC Studio.

Handle Request

Go to Logic - Integrations - REST - MessagingEndpoint and double click on the exposed Messages endpoint that handles inbound requests from the Azure Bot resource.

In a first step we have to determine the specific type of the activity sent to our Messaging Endpoint, because for now we only want to react on Link unfurling requests.

💡
You can learn more about the structure of inbound requests in the ODC with Bots for Teams introductory article.

The DeserializeInboundMinimalActivity action deserializes the request's payload and gives us the following information:

  • type - The type of activity, which is always invoke for Link unfurling.

  • name - The name of the specific invocation, which is composeExtension/queryLink for a Link unfurling request.

The following switch statement currently has only one conditional path that is executed when type is invoke and name is composeExtension/queryLink. In all other cases we just end the action flow.

DeserializeInboundQueryLinkActivity converts the request into a structure that includes extra details such as the user who sent the request, the channel used, and more. The most important detail is the URL that was pasted, which is found in the Value attribute of the InvokeQueryLinkActivity structure.

The URL is then finally used to prepare a response in the UnfurlAuctionLink action.

Unfurl

The UnfurlAuctionLink performs several actions to create a response that can be sent back to the channel (Microsoft Teams).

  • Parse - This external logic action extracts the URL from the Link unfurling request.

  • ParseQueryString - Splits the query string from the URL into a list of name-value pairs.

  • HasAuctionDetailPath - Checks if the URL is from an AuctionDetail screen by examining the segments of the parsed URL. If not, we simply exit.

  • FilterAuctionId - Filters the query string name-value pairs for the AuctionId parameter.

  • RenderAuctionLoop - Uses the URL and the identified AuctionId value to create the response (see below).

Render Auction Response

RenderAuctionLoop retrieves the Auction entity to obtain the record for the requested auction. It then uses this information to create a complete response for the Messaging Endpoint to send back to the channel.

Auction_Get - This action retrieves the record for the given AuctionId from the Auction entity.

The following actions are for merging a response template with data from the auction. The template (available under Data - Resources - AuctionUnfurl.json) contains handlebar placeholders that will be replaced by values using RenderTemplate from the LiquidFluid external logic library.

💡
LiquidFluid is a component that can combine data with a template file using the Shopify Liquid Templating language.

The template contains the following placeholders

  • title - Title of the auction

  • description - Full item description

  • price - The auction start price

  • webUrl - The full url referencing the auction in the demo application

This data structure is represented by a structure AuctionCard in Data - Structures - Auction.

First, we construct the Card payload by assigning values from Auction_Get to the structure properties. Next, we serialize the AuctionCard structure, and finally, we use the RenderTemplate action to merge the serialized data structure with the template.

The Messaging Endpoint then returns this rendered response to the channel, and the Adaptive Card-based Loop Component is displayed.

Missing Pieces

Try changing the auction you pasted earlier in the demo application. You will notice that the card does not update. Being "live" is a core feature of a Loop component, so we need to ensure that cards update when the data changes. We will address this in the next part of the tutorial series.

Another missing piece is that you can copy a Loop component between Microsoft Teams conversations, but not, for example, to a Microsoft Outlook message. We will cover this in a later part.

For now, congratulations! You have successfully turned an OutSystems application link into an Adaptive Card-based Loop component.

Summary

In this part of the ODC with Loop Components tutorial series, we set up the prerequisites for a Message Extension in Azure and implemented a Messaging Endpoint that responds to a link unfurling request from a Microsoft Teams channel with an Adaptive Card-based Loop component.

I hope you enjoyed it. Feel free to leave a comment with your questions and feedback.

0
Subscribe to my newsletter

Read articles from Stefan Weber directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Stefan Weber
Stefan Weber

As a seasoned Senior Director at Telelink Business Services EAD, a leading IT full-service provider headquartered in Sofia, Bulgaria, I lead the charge in our Application Services Practice. In this role, I spearhead the development of tailored software solutions using no-code/low-code platforms and cutting-edge cloud-ready/cloud-native solutions based on the Microsoft .NET stack. Throughout my diverse career, I've accumulated a wealth of experience in various capacities, both technically and personally. The constant desire to create innovative software solutions led me to the world of Low-Code and the OutSystems platform. I remain captivated by how closely OutSystems aligns with traditional software development, offering a seamless experience devoid of limitations. While my managerial responsibilities primarily revolve around leading and inspiring my teams, my passion for solution development with OutSystems remains unwavering. My personal focus extends to integrating our solutions with leading technologies such as Amazon Web Services, Microsoft 365, Azure, and more. In 2023, I earned recognition as an OutSystems Most Valuable Professional, one of only 80 worldwide, and concurrently became an AWS Community Builder.