Implement a skill

APPLIES TO: SDK v4

You can use skills to extend another bot. A skill is a bot that can perform a set of tasks for another bot.

  • A manifest describes a skill's interface. Developers who don't have access to the skill's source code can use the information in the manifest to design their skill consumer.
  • A skill can use claims validation to manage which bots or users can access it.

This article demonstrates how to implement a skill that echoes the user's input.

Some types of skill consumers are not able to use some types of skill bots. The following table describes which combinations are supported.

  Multi-tenant skill Single-tenant skill User-assigned managed identity skill
Multi-tenant consumer Supported Not supported Not supported
Single-tenant consumer Not supported Supported if both apps belong to same tenant Supported if both apps belong to same tenant
User-assigned managed identity consumer Not supported Supported if both apps belong to same tenant Supported if both apps belong to same tenant

Note

The Bot Framework JavaScript, C#, and Python SDKs will continue to be supported, however, the Java SDK is being retired with final long-term support ending in November 2023.

Existing bots built with the Java SDK will continue to function.

For new bot building, consider using Microsoft Copilot Studio and read about choosing the right copilot solution.

For more information, see The future of bot building.

Prerequisites

Note

Starting with version 4.11, you don't need an app ID and password to test a skill locally in the Bot Framework Emulator. An Azure subscription is still required to deploy your skill to Azure.

About this sample

The skills simple bot-to-bot sample includes projects for two bots:

  • The echo skill bot, which implements the skill.
  • The simple root bot, which implements a root bot that consumes the skill.

This article focuses on the skill, which includes support logic in its bot and adapter.

For information about the simple root bot, see how to Implement a skill consumer.

Resources

For deployed bots, bot-to-bot authentication requires that each participating bot has valid identity information. However, you can test multitenant skills and skill consumers locally with the Emulator without an app ID and password.

To make the skill available to user-facing bots, register the skill with Azure. For more information, see how to register a bot with Azure AI Bot Service.

Application configuration

Optionally, add the skill's identity information to its configuration file. If either the skill or skill consumer provides identity information, both must.

The allowed callers array can restrict which skill consumers can access the skill. To accept calls from any skill consumer, add an "*" element.

Note

If you're testing your skill locally without bot identity information, neither the skill nor the skill consumer run the code to perform claims validation.

EchoSkillBot\appsettings.json

Optionally, add the skill's identity information to the appsettings.json file.

{
  "MicrosoftAppType": "",
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "MicrosoftAppTenantId": "",

  // This is a comma separate list with the App IDs that will have access to the skill.
  // This setting is used in AllowedCallersClaimsValidator.
  // Examples: 
  //    [ "*" ] allows all callers.
  //    [ "AppId1", "AppId2" ] only allows access to parent bots with "AppId1" and "AppId2".
  "AllowedCallers": [ "*" ]
}

Activity handler logic

To accept input parameters

The skill consumer can send information to the skill. One way to accept such information is to accept them via the value property on incoming messages. Another way is to handle event and invoke activities.

The skill in this example doesn't accept input parameters.

To continue or complete a conversation

When the skill sends an activity, the skill consumer should forward the activity on to the user.

However, you need to send an endOfConversation activity when the skill finishes; otherwise, the skill consumer continues to forward user activities to the skill. Optionally, use the activity's value property to include a return value, and use the activity's code property to indicate why the skill is ending.

EchoSkillBot\Bots\EchoBot.cs

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    if (turnContext.Activity.Text.Contains("end") || turnContext.Activity.Text.Contains("stop"))
    {
        // Send End of conversation at the end.
        var messageText = $"ending conversation from the skill...";
        await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput), cancellationToken);
        var endOfConversation = Activity.CreateEndOfConversationActivity();
        endOfConversation.Code = EndOfConversationCodes.CompletedSuccessfully;
        await turnContext.SendActivityAsync(endOfConversation, cancellationToken);
    }
    else
    {
        var messageText = $"Echo: {turnContext.Activity.Text}";
        await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput), cancellationToken);
        messageText = "Say \"end\" or \"stop\" and I'll end the conversation and back to the parent.";
        await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput), cancellationToken);
    }
}

To cancel the skill

For multi-turn skills, you would also accept endOfConversation activities from a skill consumer, to allow the consumer to cancel the current conversation.

The logic for this skill doesn't change from turn to turn. If you implement a skill that allocates conversation resources, add resource cleanup code to the end-of-conversation handler.

EchoSkillBot\Bots\EchoBot.cs

protected override Task OnEndOfConversationActivityAsync(ITurnContext<IEndOfConversationActivity> turnContext, CancellationToken cancellationToken)
{
    // This will be called if the root bot is ending the conversation.  Sending additional messages should be
    // avoided as the conversation may have been deleted.
    // Perform cleanup of resources if needed.
    return Task.CompletedTask;
}

Claims validator

This sample uses an allowed callers list for claims validation. The skill's configuration file defines the list. The validator object then reads the list.

You must add a claims validator to the authentication configuration. The claims are evaluated after the authentication header. Your validation code should throw an error or exception to reject the request. There are many reasons you might want to reject an otherwise authenticated request. For example:

  • The skill is part of a paid-for service. User's not in the database shouldn't have access.
  • The skill is proprietary. Only certain skill consumers can call the skill.

Important

If you don't provide a claims validator, your bot will generate an error or exception upon receiving an activity from the skill consumer.

The SDK provides an AllowedCallersClaimsValidator class that adds application-level authorization based on a simple list of IDs of the applications that are allowed to call the skill. If the list contains an asterisk (*), then all callers are allowed. The claims validator is configured in Startup.cs.

Skill adapter

When an error occurs, the skill's adapter should clear conversation state for the skill, and it should also send an endOfConversation activity to the skill consumer. To signal that the skill ended due to an error, use the code property of the activity.

EchoSkillBot\SkillAdapterWithErrorHandler.cs

private async Task HandleTurnError(ITurnContext turnContext, Exception exception)
{
    // Log any leaked exception from the application.
    _logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");

    await SendErrorMessageAsync(turnContext, exception);
    await SendEoCToParentAsync(turnContext, exception);
}

private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception)
{
    try
    {
        // Send a message to the user.
        var errorMessageText = "The skill encountered an error or bug.";
        var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput);
        await turnContext.SendActivityAsync(errorMessage);

        errorMessageText = "To continue to run this bot, please fix the bot source code.";
        errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput);
        await turnContext.SendActivityAsync(errorMessage);

        // Send a trace activity, which will be displayed in the Bot Framework Emulator.
        // Note: we return the entire exception in the value property to help the developer;
        // this should not be done in production.
        await turnContext.TraceActivityAsync("OnTurnError Trace", exception.ToString(), "https://www.botframework.com/schemas/error", "TurnError");
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Exception caught in SendErrorMessageAsync : {ex}");
    }
}

private async Task SendEoCToParentAsync(ITurnContext turnContext, Exception exception)
{
    try
    {
        // Send an EndOfConversation activity to the skill caller with the error to end the conversation,
        // and let the caller decide what to do.
        var endOfConversation = Activity.CreateEndOfConversationActivity();
        endOfConversation.Code = "SkillError";
        endOfConversation.Text = exception.Message;
        await turnContext.SendActivityAsync(endOfConversation);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Exception caught in SendEoCToParentAsync : {ex}");
    }
}

Service registration

The Bot Framework adapter uses an authentication configuration object (set when the adapter is created) to validate the authentication header on incoming requests.

This sample adds claims validation to the authentication configuration and uses the skill adapter with error handler described in the previous section.

EchoSkillBot\Startup.cs

    options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth;
});

// Register AuthConfiguration to enable custom claim validation.
services.AddSingleton(sp =>
{
    var allowedCallers = new List<string>(sp.GetService<IConfiguration>().GetSection("AllowedCallers").Get<string[]>());

    var claimsValidator = new AllowedCallersClaimsValidator(allowedCallers);

    // If TenantId is specified in config, add the tenant as a valid JWT token issuer for Bot to Skill conversation.
    // The token issuer for MSI and single tenant scenarios will be the tenant where the bot is registered.
    var validTokenIssuers = new List<string>();
    var tenantId = sp.GetService<IConfiguration>().GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value;

    if (!string.IsNullOrWhiteSpace(tenantId))
    {
        // For SingleTenant/MSI auth, the JWT tokens will be issued from the bot's home tenant.
        // Therefore, these issuers need to be added to the list of valid token issuers for authenticating activity requests.
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId));
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, tenantId));
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV1, tenantId));
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2, tenantId));
    }

    return new AuthenticationConfiguration
    {
        ClaimsValidator = claimsValidator,
        ValidTokenIssuers = validTokenIssuers
    };
});

// Create the Bot Framework Authentication to be used with the Bot Adapter.
services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

Skill manifest

A skill manifest is a JSON file that describes the activities the skill can perform, its input and output parameters, and the skill's endpoints. The manifest contains the information you need to access the skill from another bot. The latest schema version is v2.1.

EchoSkillBot\wwwroot\manifest\echoskillbot-manifest-1.0.json

{
  "$schema": "https://schemas.botframework.com/schemas/skills/skill-manifest-2.0.0.json",
  "$id": "EchoSkillBot",
  "name": "Echo Skill bot",
  "version": "1.0",
  "description": "This is a sample echo skill",
  "publisherName": "Microsoft",
  "privacyUrl": "https://echoskillbot.contoso.com/privacy.html",
  "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
  "license": "",
  "iconUrl": "https://echoskillbot.contoso.com/icon.png",
  "tags": [
    "sample",
    "echo"
  ],
  "endpoints": [
    {
      "name": "default",
      "protocol": "BotFrameworkV3",
      "description": "Default endpoint for the skill",
      "endpointUrl": "http://echoskillbot.contoso.com/api/messages",
      "msAppId": "00000000-0000-0000-0000-000000000000"
    }
  ]
}

The skill manifest schema is a JSON file that describes the schema of the skill manifest. The current schema version is 2.1.0.

Test the skill

At this point, you can test the skill in the Emulator as if it were a normal bot. However, to test it as a skill, you would need to implement a skill consumer.

Download and install the latest Bot Framework Emulator

  1. Run the echo skill bot locally on your machine. If you need instructions, refer to the README file for the C#, JavaScript, Java, or Python sample.
  2. Use the Emulator to test the bot. When you send an "end" or "stop" message to the skill, it sends an endOfConversation activity in addition to the reply message. The skill sends the endOfConversation activity to indicate the skill is finished.

Example transcript showing the end-of-conversation activity.

More about debugging

Since traffic between skills and skill consumers is authenticated, there are extra steps when debugging such bots.

  • The skill consumer and all the skills it consumes, directly or indirectly, must be running.
  • If the bots are running locally and if any of the bots has an app ID and password, then all bots must have valid IDs and passwords.
  • If the bots are all deployed, see how to Debug a bot from any channel using devtunnel.
  • If some of the bots are running locally and some are deployed, then see how to Debug a skill or skill consumer.

Otherwise, you can debug a skill consumer or skill much like you debug other bots. For more information, see Debugging a bot and Debug with the Bot Framework Emulator.

Next steps