Home Services Work About Blog Contact Let's Talk
Blog / Microsoft Teams
Microsoft Teams

Teams Message Extensions with SPFx: Search Commands & Action Commands

What Are Teams Message Extensions and Why They Matter

Teams message extensions are integration points that allow users to interact with external systems and SharePoint data without ever leaving a Teams conversation. They appear in the compose box, in the action menu on existing messages, and in the command bar — three distinct entry points that cover virtually every moment in a user's chat workflow. A well-built message extension can transform a Teams channel from a communication tool into an operational hub where employees search knowledge bases, log tasks, and surface records directly inside their conversations.

From a developer's perspective, message extensions are implemented as Bot Framework bots that respond to structured invoke activities. When a user types a search query or submits an action form, Teams sends an invoke payload to your bot endpoint. The bot processes the request — querying SharePoint, calling an API, creating a list item — and returns either a list of result cards (search command) or a confirmation adaptive card (action command). The key architectural insight is that the bot is stateless; every invoke is independent, which makes message extensions highly scalable.

SPFx enters the picture as the packaging and surfacing mechanism. Rather than deploying a standalone Teams app, you can package your message extension alongside SPFx web parts in a single .sppkg solution, deploy through the App Catalog, and have it available in Teams automatically via the SharePoint-to-Teams sync. This is the approach we recommend for organisations already invested in the SPFx ecosystem — it keeps app governance centralised and avoids the complexity of maintaining a separate Teams app registration and hosting infrastructure.

Teams App Manifest Setup for Message Extensions

Every Teams app begins with a manifest — a JSON file that declares your app's identity, capabilities, and command definitions. For message extensions, the critical section is composeExtensions. Each entry in this array corresponds to one bot, and within it you define your command set. You can mix search commands and action commands within a single compose extension, which is useful when your extension serves multiple workflows (search for existing records, then action-command to create a new one).

The manifest schema for Teams apps moved to version 1.16 in 2024 and introduced the authorization block for RSC (resource-specific consent) permissions. If your message extension needs to read channel messages or access user profiles, you must declare the appropriate RSC permissions here rather than relying on delegated Graph scopes alone. This is a common source of permission errors in enterprise tenants where the Teams admin has restricted app consent.

JSON — Teams App Manifest (message extension excerpt)
{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
  "manifestVersion": "1.16",
  "id": "<your-app-guid>",
  "name": { "short": "SharePoint Search" },
  "composeExtensions": [
    {
      "botId": "<your-bot-app-id>",
      "commands": [
        {
          "id": "searchSharePoint",
          "type": "query",
          "title": "Search SharePoint",
          "description": "Find documents and list items",
          "parameters": [
            {
              "name": "searchQuery",
              "title": "Search Query",
              "description": "Enter keywords to search",
              "inputType": "text"
            }
          ]
        },
        {
          "id": "createListItem",
          "type": "action",
          "title": "Create Task",
          "description": "Log a task from this message",
          "fetchTask": true,
          "context": ["message", "compose"]
        }
      ]
    }
  ]
}
Tip

Set "fetchTask": true for action commands to load the task module dynamically via a bot invoke — this lets you pre-populate form fields with context from the selected message before the user sees the dialog.

Bot Framework Registration in Azure

Message extensions require a registered Azure Bot resource. Navigate to the Azure portal, create a new Azure Bot resource, and note the App ID. During creation you choose between a single-tenant and multi-tenant bot — for internal enterprise tools, single-tenant is the correct choice as it restricts the bot to your organisation's Azure AD. Set the messaging endpoint to your bot's hosted URL; during development this will be an ngrok tunnel, in production it should be your Azure Function or App Service endpoint over HTTPS.

After registration, navigate to the bot's Channels blade and enable the Microsoft Teams channel. Then go to Configuration and generate a client secret. Store the App ID and client secret securely — you will need them in your bot's environment variables. For SPFx-hosted message extensions, these credentials are typically stored in Azure Key Vault and referenced through managed identity, keeping secrets out of source code entirely.

TypeScript — Bot adapter initialisation
import { ConfigurationServiceClientCredentialFactory, createBotFrameworkAuthenticationFromConfiguration } from 'botframework-connector';
import { CloudAdapter } from 'botbuilder';

const credentialsFactory = new ConfigurationServiceClientCredentialFactory({
  MicrosoftAppId: process.env.BOT_ID,
  MicrosoftAppPassword: process.env.BOT_PASSWORD,
  MicrosoftAppType: 'SingleTenant',
  MicrosoftAppTenantId: process.env.TENANT_ID,
});

const auth = createBotFrameworkAuthenticationFromConfiguration(null, credentialsFactory);
export const adapter = new CloudAdapter(auth);

adapter.onTurnError = async (context, error) => {
  console.error('[onTurnError]', error);
  await context.sendActivity('An error occurred. Please try again.');
};

Building the Search Command Handler

The search command handler is the heart of a query-type message extension. When a user types in the Teams search box, Teams sends a composeExtension/query invoke activity to your bot approximately 300 ms after the user stops typing. Your handler must respond within 5 seconds or Teams will display a timeout error — so efficient SharePoint querying is non-negotiable. Use the SharePoint Search REST API with a pre-indexed managed property rather than list queries for best performance.

The response from a search command must be an InvokeResponse with a composeExtension body containing an array of attachment objects. Each attachment maps to a card in the search results list. You can use Hero cards, Thumbnail cards, or Adaptive Cards — Adaptive Cards give the most visual flexibility and are the recommended format for new extensions.

TypeScript — Search command invoke handler
protected async handleTeamsMessagingExtensionQuery(
  context: TurnContext,
  query: MessagingExtensionQuery
): Promise<MessagingExtensionResponse> {
  const searchQuery = query.parameters?.[0]?.value ?? '';

  // Hit SharePoint Search REST API
  const token = await this.getSharePointToken();
  const results = await searchSharePoint(searchQuery, token);

  const attachments = results.map(item => {
    const card = CardFactory.adaptiveCard({
      type: 'AdaptiveCard', version: '1.5',
      body: [
        { type: 'TextBlock', text: item.Title, weight: 'Bolder', size: 'Medium', wrap: true },
        { type: 'TextBlock', text: item.Author, size: 'Small', isSubtle: true },
        { type: 'TextBlock', text: item.HitHighlightedSummary, wrap: true, maxLines: 3 },
      ],
      actions: [{ type: 'Action.OpenUrl', title: 'Open', url: item.Path }],
    });
    return { ...card, preview: CardFactory.thumbnailCard(item.Title, item.Author) };
  });

  return { composeExtension: { type: 'result', attachmentLayout: 'list', attachments } };
}

Building the Action Command Handler

Action commands present a task module — a modal dialog — to the user. With fetchTask: true, Teams first sends a composeExtension/fetchTask invoke to your bot. This is your opportunity to build a dynamic Adaptive Card form pre-populated with data from the message context. The message body, sender name, and timestamp are all available in the invoke payload, making it trivial to pre-fill a "Create Task" form with the message content as the task description.

When the user submits the form, Teams sends a composeExtension/submitAction invoke. Your handler receives the form data, calls the SharePoint REST API to create the list item, and returns either a confirmation card to insert into the conversation or a simple message acknowledgement. The ability to close the loop — action taken, confirmation visible in chat — is what makes action commands so powerful for operational workflows.

TypeScript — fetchTask and submitAction handlers
protected async handleTeamsMessagingExtensionFetchTask(
  context: TurnContext,
  action: MessagingExtensionAction
): Promise<MessagingExtensionActionResponse> {
  const messageText = action.messagePayload?.body?.content ?? '';
  return {
    task: {
      type: 'continue',
      value: {
        title: 'Create SharePoint Task', height: 400, width: 500,
        card: CardFactory.adaptiveCard({
          type: 'AdaptiveCard', version: '1.5',
          body: [
            { type: 'Input.Text', id: 'title', label: 'Task Title', isRequired: true },
            { type: 'Input.Text', id: 'description', label: 'Description',
              value: messageText, isMultiline: true },
            { type: 'Input.ChoiceSet', id: 'priority', label: 'Priority',
              choices: [
                { title: 'High', value: 'High' },
                { title: 'Medium', value: 'Medium' },
                { title: 'Low', value: 'Low' },
              ]}
          ],
          actions: [{ type: 'Action.Submit', title: 'Create Task' }],
        }),
      },
    },
  };
}

protected async handleTeamsMessagingExtensionSubmitAction(
  context: TurnContext,
  action: MessagingExtensionAction
): Promise<MessagingExtensionActionResponse> {
  const { title, description, priority } = action.data;
  await createSharePointListItem({ title, description, priority });
  return {
    composeExtension: {
      type: 'result', attachmentLayout: 'list',
      attachments: [CardFactory.adaptiveCard({
        type: 'AdaptiveCard', version: '1.5',
        body: [{ type: 'TextBlock', text: `Task "${title}" created successfully.`, weight: 'Bolder' }],
      })],
    },
  };
}

Crafting the Adaptive Card Response

The Adaptive Card you return from a message extension is special: it gets inserted into the Teams conversation as a rich attachment. Unlike Quick Views in ACEs, these cards are persistent — they remain in the chat history and can be forwarded, referenced, and pinned. Design them to be self-contained and informative without requiring the reader to open a link to understand the context.

For search result cards, the key design principle is information hierarchy: title prominent, supporting metadata subtle, and a single clear call-to-action. Avoid cramming multiple actions into a search result card — save secondary actions for a detail view opened via a link. For action confirmation cards inserted into chat, always include the key details of what was just created or updated so the conversation thread stays auditable without requiring colleagues to open SharePoint.

Watch Out

Teams renders Adaptive Cards in message extensions at schema version 1.5. Do not use Action.Execute (Universal Actions) in search result cards — it is only supported in bot-sent cards, not in compose extension attachments.

SPFx Integration and Packaging

To deploy a message extension as part of an SPFx solution, you add the Teams app manifest and bot registration to the SPFx project's teams/ folder. The Yeoman generator can scaffold the Teams folder structure when you select "Microsoft Teams" as a deployment target. The config/package-solution.json must include the Teams app in the features array so the App Catalog provisions it automatically when you deploy the .sppkg.

The practical limitation of the SPFx packaging approach is that the bot must be hosted externally — SPFx itself cannot host a Node.js bot endpoint. We typically deploy the bot as an Azure Function with an HTTP trigger. The Function URL is set as the messaging endpoint in the Azure Bot registration. Azure Functions on a Consumption plan handle the bursty traffic pattern of message extension invokes very efficiently, and costs for internal enterprise tools are typically negligible.

JSON — package-solution.json Teams integration
{
  "solution": {
    "name": "sharepoint-message-extension-client-side-solution",
    "id": "<solution-guid>",
    "version": "1.0.0.0",
    "includeClientSideAssets": true,
    "skipFeatureDeployment": true,
    "isDomainIsolated": false
  },
  "paths": {
    "zippedPackage": "solution/sharepoint-message-extension.sppkg"
  }
}

Deployment, Testing, and Admin Approval

Deployment follows the standard SPFx path: gulp bundle --ship && gulp package-solution --ship, then upload the .sppkg to the App Catalog. The Teams admin must separately approve the Teams app in the Teams Admin Center under Manage apps. Until approved, the message extension will not appear in Teams even though the SPFx solution is deployed to SharePoint.

For testing during development, use the Teams Developer Portal to sideload the manifest directly into a test team. This bypasses the admin approval flow and lets you iterate quickly. Combine sideloading with ngrok for the bot endpoint and you have a complete local development loop. We recommend using the App Test Tool in Teams Toolkit — it simulates the invoke payloads that Teams sends, letting you test your bot handler logic in isolation without needing a full Teams client.

Once in production, monitor your bot's health through Azure Application Insights. Set up an alert on the requests/failed metric — message extension timeouts and unhandled exceptions produce 5xx responses that will be logged here. The most common production failure is token expiry in the SharePoint access token cache; implement a token refresh with a 5-minute buffer before expiry and you will eliminate the majority of runtime errors.

Key Takeaways

Message extensions are stateless Bot Framework bots — every invoke is independent, making them highly scalable. Design your handler to respond within 5 seconds or Teams will show a timeout.

Use the SharePoint Search REST API with managed properties for query commands — list queries are too slow for the sub-300ms response window Teams expects.

Action commands with fetchTask: true let you pre-populate forms with message context — use this to pre-fill task descriptions with the selected message body.

Package the message extension inside your SPFx solution for centralised App Catalog governance, but host the bot endpoint on Azure Functions separately.

Teams admin approval in the Teams Admin Center is required before the extension is visible to users — factor this into your deployment timeline.

AT

Akshara Technologies

Microsoft 365 Development Specialists

With 10+ years building enterprise SharePoint, SPFx, Power Automate, and Flutter solutions for clients across India, USA, UAE, and Australia — we write from production experience, not documentation.

Related Articles

Ready to Build with Microsoft Teams?

From message extensions to full intranet portals — Akshara Technologies delivers enterprise-grade Microsoft 365 solutions that actually work.

Start Your Project View Case Studies