Bringing Bots into an Omni-channel Customer Service Strategy – Part 2: Creating Cases


This post is the second in a series in which we explore how we can take advantage of the benefits of scale and channel breadth with bots, making sure the experiences we create with them build upon the investments that we have made in our customer service strategy, and feed into the consolidated customer 360 view using Microsoft Service and Dynamics CRM as our underlying customer engagement platform.

We will build upon the bot we built in Part 1, which allows users to access self-service knowledge, tapping into the authoritative knowledge that has been curated and published using the robust knowledge management capabilities in the Dynamics CRM Interactive Service Hub.

We will upgrade our bot from v1 of the Bot Framework to v3. We will also enable our bot to collect the necessary information to create a case in Dynamics CRM, for subsequent follow-up.

We will use the concept of a FormFlow dialog within our bot to guide the user through a conversational experience to collect the information we require to submit our case to Dynamics CRM via the Web API. We will draw on the following resources:

 

Note: It is recommended that you review the concepts in Part 1 of the series for an introduction and background.

 

Pre-requisites

The pre-requisites for building our service bot include:

  • An instance of Dynamics CRM Online; you can request a trial of Dynamics CRM Online here
  • Administrator credentials to access the O365 Admin Portal for our tenant that our CRM instance is connected to
  • A Microsoft Azure subscription for application registration; A trial account will also work; See Part 1 for a walkthrough of how to obtain CRM Web API credentials
  • Dynamics CRM credentials for a user with privileges to allow us to read Knowledge Articles and create Cases in CRM
  • Visual Studio 2015
    • Update all Visual Studio extensions using Tools->Extensions and Updates->Updates
  • A Microsoft Account
  • The Bot Builder SDK
    • You can download the SDK on GitHub here
  • The v3 Bot Framework Channel Emulator (for Windows)
    • You can download the new Bot Framework Channel Emulator from here
  • The v3 Bot Connector Visual Studio Project Template
    • You can download the template from here
    • Save the downloaded zip file to your Visual Studio 2015 templates directory, which is typically found in %USERPROFILE%\Documents\Visual Studio 2015\Templates\ProjectTemplates\Visual C#

 

Updating our Bot to the Bot Framework v3

In this walkthrough, we will create a new project using the updated v3 Bot Connector Visual Studio Project Template, then bring in and adapt our v1 bot code.

We Open Visual Studio, and create a new project using the new Bot Application template that we downloaded as a part of our pre-requisites.

As before, we need install several packages that we will leverage as a part of our bot code. We can do this via NuGet. The packages include:

 

We will place the CRM Web API Credentials (obtained in Part 1 of the series) into our Web.config settings. We open the file, and add our credentials as appSettings as shown below, to make them accessible throughout our project:

<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=301879
  -->
<configuration>
  <appSettings>
    <!-- update these with your BotId, Microsoft App Id and your Microsoft App Password-->
    <add key="BotId" value="YourBotId" />
    <add key="MicrosoftAppId" value="" />
    <add key="MicrosoftAppPassword" value="" />
    <!-- update these with your CRM authentication details -->
    <add key="crmUsername" value="youruser@yourorg.onmicrosoft.com" />
    <add key="crmPassword" value="yourPassword" />
    <add key="crmUri" value="https://yourorg.crm.dynamics.com" />
    <add key="adOath2AuthEndpoint" value="https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/oauth2/authorize" />    
    <add key="adClientId" value="youradClientId" />  
  </appSettings>
...

 

Next, we will adapt our MessagesController.cs class to be able to initiate our main dialog with the user, just as in Part 1. With the v3 bot framework, however, the Message object has been replaced with the Activity object. We replace the default Echo Bot code with code that will initiate a new Dialog using the ServiceDialog class from Part 1:

using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.Dialogs;


namespace ServiceBotV3
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        /// <summary>
        /// POST: api/Messages
        /// Receive a message from a user and reply to it
        /// </summary>
        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                await Conversation.SendAsync(activity, () => new ServiceDialog());
            }
            else
            {
                HandleSystemMessage(activity);
            }
            var response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }
....

 

This time around, we will separate our ServiceDialog into a separate class file. To our project, we Add a new C# class called ServiceDialog.cs.

Into our class, we insert the code from Part 1, but we make some minor changes:

  • Any instances of Message objects are updated to Activity objects
  • We make use of the PostAsync method to send messages that are not prompting the user for information
  • We add two additional parameters to our call to the CRMWebAPIRequest method, to allow us to specify our HTTP Method, and pass a payload, to cater for our future requirements to create cases in our bot code
  • We set a value in our ConversationData with the Guid of one of our CRM Contacts
    • As an extension of this example, we could add code to authenticate our user, and obtain their contact Guid, or alternately, create a new contact record based on data collected from the user

 

At this point, our updated ServiceDialog.cs class should appear as shown below:

using System;
using System.Linq;
using System.Net.Http;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using Microsoft.Bot.Builder.FormFlow.Json;
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
using System.Reflection;
using System.IO;

namespace ServiceBotV3
{
    // As per the Dialogs model example in the Bot Builder docs,
    //  we add a class to represent our conversation:
    [Serializable]
    public class ServiceDialog : IDialog<object>
    {

        // Start the dialog:
        public async Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);
        }

        // Handle an initial message from the user
        public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
        {
            // set customer id in conversation data. In a real-world scenario, this would be obtained
            //  via authenticating the user, or we would gather contact information to create a contact in CRM:
            //  Replace the Guid with one of your contacts for testing purposes:
            context.ConversationData.SetValue<Guid>("contactid", new Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"));

            // Create a list of options:
            System.Collections.Generic.IList<string> startOptions = new System.Collections.Generic.List<string>();
            startOptions.Add("Information");
            startOptions.Add("Submit an Issue");
            startOptions.Add("Track Cases");
            // Present the options to the user, with a continuation back to the appropriate handler:
            PromptDialog.Choice(context, AfterServiceChoiceAsync,
                new PromptOptions<string>("How can I help you?", null, null, startOptions, 3, null));
        }

        // handler for the initial user selection:
        public async Task AfterServiceChoiceAsync(IDialogContext context, IAwaitable<string> argument)
        {
            var option = await argument;
            if (option == "Information")
            {
                // prompt the user to provide a search query, and specify the appropriate handler:
                PromptDialog.Text(context, AfterSearchTermProvidedAsync, "What are you interested in?", null, 3);
            }
            else
            {
                // We will implement these capabilities in a future post
                // In the meantime, include a notification:
                await context.PostAsync("I will be able to help you with those things soon. Stay tuned.");
                context.Wait(MessageReceivedAsync);
            }

        }

        // handler for the 'search again' prompt selection:
        public async Task AfterSearchAgainAsync(IDialogContext context, IAwaitable<bool> argument)
        {
            var option = await argument;
            if (option == true)
            {
                PromptDialog.Text(context, AfterSearchTermProvidedAsync, "What are you interested in?", null, 3);
            }
            else
            {
                await context.PostAsync("Thanks. Let me know if I can help you again.");
                context.Wait(MessageReceivedAsync);
            }

        }
        public async Task AfterSearchTermProvidedAsync(IDialogContext context, IAwaitable<string> argument)
        {
            // retrieve the query term provided by the user:
            string keyword = await argument;

            // Note that the Web API currently offers limited knowledgebase searching capabilities.
            // The Organization Service offers the ability to perform a full-text search on articles
            //  with the FullTextSearchKnowledgeArticleRequest Message request class.
            //  See here for more info: 
            //  https://msdn.microsoft.com/en-us/library/microsoft.crm.sdk.messages.fulltextsearchknowledgearticlerequest.aspx
            // We will use a basic 'contains' filter on the keywords of the articles in this sample,
            //  also filtering on only published articles:
            HttpResponseMessage kbResponse = await Utilities.CRMWebAPIRequest("api/data/v8.1/knowledgearticles?" +
                "$select=title,articlepublicnumber,description&$top=3&$filter=statecode eq 3 and " +
                "contains(keywords, '" + keyword + "')", null, "retrieve");

            // Check to make sure our request was successful:
            if (kbResponse.IsSuccessStatusCode)
            {
                // Read our response into a JSON Array:
                string myString = kbResponse.Content.ReadAsStringAsync().Result;
                JObject kbResults =
                    JObject.Parse(kbResponse.Content.ReadAsStringAsync().Result);
                JArray items = (JArray)kbResults["value"];
                JObject item;

                // Ensure we have some results:
                if (items.Count > 0)
                {
                    // For Web Chat, we can use Markdown to format our messages.
                    //  For more information, see:
                    //  http://docs.botframework.com/connector/message-content/#the-text-property-is-markdown 

                    IMessageActivity msgMarkdownReply = context.MakeMessage();
                    msgMarkdownReply.Text = "I found some articles:  ";

                    // loop through our results:
                    for (int i = 0; i < items.Count; i++)
                    {
                        item = (JObject)items[i];
                        // Add our article title, with a link to to the full article
                        //  We are assuming that we have configured a portal that we can direct
                        //  our user to. You will need to adapt your URL structure based on your
                        //  portal configuration:
                        msgMarkdownReply.Text += Environment.NewLine + Environment.NewLine +
                            "[" + (string)item["title"] + "](" + "http://myexampleportal.com/" +
                            (string)item["articlepublicnumber"] + ")  ";
                        // Include the article description, if we have one:
                        string description = (string)item["description"];
                        if (String.IsNullOrEmpty(description) == false)
                        {
                            msgMarkdownReply.Text += Environment.NewLine + Environment.NewLine + "*" + (string)item["description"] + "*";
                        }
                    }

                    await context.PostAsync(msgMarkdownReply);

                    // We also use a prompt to confirm whether our user would like to search again:
                    PromptDialog.Confirm(context, AfterSearchAgainAsync, "Would you like to search again?");

                }
                else
                {
                    PromptDialog.Confirm(context, AfterSearchAgainAsync, "I couldn't find any articles. Would you like to search again?");
                }

            }
            else
            {
                PromptDialog.Confirm(context, AfterSearchAgainAsync, "There was an error searching. Would you like to try again?");
            }
        }

    }
}

 

We will also create a separate class to hold utility methods that may be accessed throughout our solution, such as the CRMWebAPIRequest method, used to issue our Web API requests to CRM. This method is taken from our Part 1 project, but adapted slightly, with these changes:

  • The method accepts not only a request string, but also an HttpContent request payload, and request type indicator, to allow for not only querying, but creation of records in CRM
  • CRM Web API credentials are retrieved from the Web.config app
  • Depending on whether the request type, there is conditional logic to issue either a GET request when we are retrieving CRM data, or a POST request with content, if we are creating a CRM record

 

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web.Configuration;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace ServiceBotV3
{
    public class Utilities
    {
        // Take a CRM Web API request string (and optionally json data), issue it, and return the HTTP Response:
        public static async Task<HttpResponseMessage> CRMWebAPIRequest(string apiRequest, HttpContent requestContent, string requestType)
        {
            AuthenticationContext authContext = new AuthenticationContext(WebConfigurationManager.AppSettings["adOath2AuthEndpoint"], false);
            UserCredential credentials = new UserCredential(WebConfigurationManager.AppSettings["crmUsername"],
                WebConfigurationManager.AppSettings["crmPassword"]);
            AuthenticationResult tokenResult = authContext.AcquireToken(WebConfigurationManager.AppSettings["crmUri"],
                WebConfigurationManager.AppSettings["adClientId"], credentials);
            HttpResponseMessage apiResponse;

            using (HttpClient httpClient = new HttpClient())
            {
                httpClient.BaseAddress = new Uri(WebConfigurationManager.AppSettings["crmUri"]);
                httpClient.Timeout = new TimeSpan(0, 2, 0);
                httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
                httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
                httpClient.DefaultRequestHeaders.Accept.Add(
                    new MediaTypeWithQualityHeaderValue("application/json"));
                httpClient.DefaultRequestHeaders.Authorization =
                    new AuthenticationHeaderValue("Bearer", tokenResult.AccessToken);

                if (requestType == "retrieve")
                {
                    apiResponse = await httpClient.GetAsync(apiRequest);
                }
                else if (requestType == "create")
                {
                    apiResponse = await httpClient.PostAsync(apiRequest, requestContent);
                }
                else
                {
                    apiResponse = null;
                }
            }
            return apiResponse;
        }
    }
}

 

With these changes, our bot will now function as before, enabling access to self-service knowledge using the v3 Bot Framework:

KBSearchV3

 

Augmenting our Bot with Case Creation Capabilities

We will now enhance the functionality of our bot by enabling end-users to log issues via a conversational bot experience, resulting in cases in Dynamics CRM. As noted earlier, we will leverage a different type of dialog called FormFlow. FormFlow dialogs offer the ability to gather the information required to achieve a particular objective in a simplified way, based on a definition you (the developer) create in either a C# class or JSON object. The Bot Framework documentation provides a thorough discussion of the use of FormFlow, and serves as a great reference for in-depth information. The gathering of information required to submit a case is a process well-suited to the FormFlow model.

We will use the JSON Schema FormFlow in this walkthrough. One benefit of using this option is that, although in our basic example, we will be hard-coding certain attributes we will collect for our CRM Case within our JSON file, we could potentially adapt the entire set of data we need to capture at run-time, based on our CRM schema, through the dynamic creation of our JSON schema, allowing for extensibility.

First, we will add a new file to our project, by right-clicking on the project in the Visual Studio Solution Explorer, then selecting Add… > New Item. We then select a JSON Schema File template, under the Visual C# > Web options:

NewJSONSchema

 

We also select our file in the Solution Explorer, and change the Build Action property to be Embedded Resource, to allow our file to be used to create our FormFlow dialog once built:

JSONProperties

 

In our JSON Schema File, we define the pieces of data that we need to collect. For each attribute we wish to collect, we are able to specify a prompt, a data type, options, validation, and other aspects. We define the information we wish to collect as described below:

  • a Title for the case: a string will be typed by the user
  • a Product that the case relates to: the user will be prompted to select from three options
    • Note that the option values correspond to the Guid values of three of our products in our instance of CRM; we are able to present the appropriate Display Names for the products using the DescribeAttribute in our schema
    • As an extension of this example, in a real-world scenario, these options could potentially be populated based on products the customer is known to own
  • a Priority for the case: the user will be prompted to select from four options
    • Note that the option values correspond to the values of the Option Set used for the prioritycode field in our CRM instance; we again provide appropriate Display Names for the options
  • a Description of the issue being experienced: a string will be typed by the user

{
  "type": "object",
  "required": [
    "title",
    "prioritycode",
    "description",
    "productid"
  ],
  "Templates": {
    "NotUnderstood": {
      "Patterns": [ "I do not understand \"{0}\".", "Try again, I don't get \"{0}\"." ]
    },
    "EnumSelectOne": {
      "Patterns": [ "Please let me know the {&}: {||}" ],
      "ChoiceStyle": "PerLine"
    }
  },
  "properties": {
    "title": {
      "Prompt": { "Patterns": [ "Can you please provide me with a {&} of the issue?" ] },
      "type": [
        "string",
        "null"
      ],
      "Validate": "var result = new ValidateResult{ IsValid = true, Value = value}; var title = (value as string).Trim(); if (title.Length == 0) {result.Feedback = \"Please provide a brief description of the issue.\"; result.IsValid = false; } return result;"
    },
    "productid": {
      "Prompt": {
        "Patterns": [ "Which of your products does this relate to? {||}" ]
      },
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "2D2D9A60-E9C2-E411-80E5-C4346BAD2660",
        "3509D8AF-7BC3-E411-80DF-FC15B42886E8",
        "58B9A158-D3C6-E411-80E1-C4346BAD461C"
      ],
      "Values": {
        "3509D8AF-7BC3-E411-80DF-FC15B42886E8": {
          "Describe": "Carbon Fiber 3D Printer"
        },
        "2D2D9A60-E9C2-E411-80E5-C4346BAD2660": {
          "Describe": "Fabrikam Tablet M200"
        },
        "58B9A158-D3C6-E411-80E1-C4346BAD461C": {
          "Describe": "Smart Extruder for 3D printer"
        }
      }
    },
    "prioritycode": {
      "Prompt": {
        "Patterns": [ "What is the urgency of this issue? {||}" ]
      },
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "3",
        "2",
        "1",
        "0"
      ],
      "Values": {
        "3": {
          "Describe": "Low"
        },
        "2": {
          "Describe": "Normal"
        },
        "1": {
          "Describe": "High"
        },
        "0": {
          "Describe": "Critical"
        }
      }
    },
    "description": {
      "type": [
        "string",
        "null"
      ],
      "Validate": "var result = new ValidateResult{ IsValid = true, Value = value}; var description = (value as string).Trim(); if (description.Length == 0) {result.Feedback = \"Please provide some more details:\"; result.IsValid = false; } return result;"
    }

  },
  "OnCompletion": "await context.PostAsync(\"I am submitting your issue, and will confirm momentarily.\");"
}

 

We now need to instantiate our Case Creation FormFlow dialog as a child dialog of our ServiceDialog at the appropriate juncture. To do this, we create a new method in our ServiceDialog.cs class, called BuildJsonForm, which will read the JSON schema we defined, and create and return a new JSON form builder:

// For the sample application in the Bot Framework samples that this was adapted from,
//  see https://github.com/Microsoft/BotBuilder/tree/master/CSharp/Samples/AnnotatedSandwichBot
public static IForm<JObject> BuildJsonForm()
{
    using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ServiceBotV3.CaseCreate.json"))
    {
        var schema = JObject.Parse(new StreamReader(stream).ReadToEnd());
        return new FormBuilderJson(schema)
            .AddRemainingFields()
            .Build();
    }
}

 

Next, we will add a new CompletedCaseSubmission method to our ServiceDialog.cs class, which will process the JObject that is generated by the FormFlow dialog that gathers the case attributes. This method will receive the context and the case creation JObject as parameters, and will:

  • Retrieve the contacted Guid from conversation data; note that for our example, we are hardcoding a contactid in our Web.config appSettings, but in a real-world scenario, we would likely authenticate our user, or collect their contact information as a part of the dialog
  • Generate some JSON content for our request to the CRM Web API, to allow us to create an incident record in CRM, populating data we collected during our FormFlow dialog
    • Important note: we include a custom caseorigincode in our example; If you wish to use the sample code with your instance of CRM, you will need to add a new caseorigincode Option Set Value of 100000001
  • Build a request Uri for our request to the CRM Web API (see CrmWebApiCSharp on GitHub for resources on interacting with the CRM Web API via C#)
  • Issue the request via our CRMWebAPIRequest method
  • Retrieve the resulting Guid for our new incident in CRM from the response
  • Use that new incident Guid to retrieve our newly-created customer-facing ticketnumber value via another “retrieve” request to the CRM Web API
  • Present the ticketnumber value back to our customer via a message back to the user
  • Finish the dialog using the IDialogStack.Done method

private async Task CompletedCaseSubmission(IDialogContext context, IAwaitable<JObject> result)
{
    // get customer id in conversation data:
    Guid contactid = context.ConversationData.Get<Guid>("contactid");
    try
    {
        var completed = await result;

        // Build JSON request content, using data obtained during FormFlow
        //  in 'completed' JObject:
        var stringContent = new StringContent("{" +
            "\"title\":\"" + (string)completed["title"] + "\"," +
            "\"prioritycode\":" + (string)completed["prioritycode"] + "," +
            "\"caseorigincode\":100000001," +
            "\"description\":\"" + (string)completed["description"] + "\"," +
            "\"productid@odata.bind\": \"/products(" + (string)completed["productid"] + ")" + "\"," +
            "\"customerid_contact@odata.bind\":\"/contacts(" + contactid.ToString() + ")\"" +
            "}", System.Text.Encoding.UTF8, "application/json");
        HttpResponseMessage caseResponse = await Utilities.CRMWebAPIRequest("api/data/v8.1/incidents", stringContent, "create");

        // Get the Case Number to return to the user:
        if (caseResponse.IsSuccessStatusCode)
        {
            Guid caseId = new Guid();
            string caseDetailsRequest = "";
            string caseUri = caseResponse.Headers.GetValues("OData-EntityId").FirstOrDefault();
            if (caseUri != null)
            {
                caseId = Guid.Parse(caseUri.Split('(', ')')[1]);
                caseDetailsRequest = "api/data/v8.1/incidents(" +
                    caseId + ")?" + "$select=ticketnumber";

                // issue a retrieve request for our case details:
                HttpResponseMessage caseDetailsResponse = await Utilities.CRMWebAPIRequest(caseDetailsRequest, null, "retrieve");

                // Check to make sure our request was successful:
                if (caseDetailsResponse.IsSuccessStatusCode)
                {
                    // Read our response into a JSON Array:
                    string myString = caseDetailsResponse.Content.ReadAsStringAsync().Result;
                    JObject caseResults =
                        JObject.Parse(caseDetailsResponse.Content.ReadAsStringAsync().Result);
                    string ticketNumber = (string)caseResults["ticketnumber"];

                    // Inform the user of the new case that has been rated:
                    await context.PostAsync("Your issue has been submitted. Your Case Number is: __" + ticketNumber + "__ .");
                }
                else
                {
                    await context.PostAsync("An error occurred while submitting your issue.");
                }
            }
            else
            {
                await context.PostAsync("An error occurred while submitting your issue.");
            }
        }
    }
    catch (FormCanceledException<JObject> e)
    {
        string reply;
        if (e.InnerException == null)
        {
            reply = $"You quit on {e.Last}--maybe you can finish next time!";
        }
        else
        {
            reply = "Sorry, I've had a short circuit.  Please try again.";
        }
        await context.PostAsync(reply);
    }

    // We sign-off with our customer, and close out the dialog:
    await context.PostAsync("If you need any assistance in the future, you know where to find me.");
    context.Done<bool>(true);
}

 

We now adapt our AfterServiceChoiceAsync method, to create a child dialog to capture case details and create the case when the user selects Submit an Issue from the initial options presented. This is the new augmented method:

// handler for the initial user selection:
public async Task AfterServiceChoiceAsync(IDialogContext context, IAwaitable<string> argument)
{
    var option = await argument;
    if (option == "Information")
    {
        // prompt the user to provide a search query, and specify the appropriate handler:
        PromptDialog.Text(context, AfterSearchTermProvidedAsync, "What are you interested in?", null, 3);
    }
    else if (option == "Submit an Issue")
    {
        // create our FormFlow dialog and add it as a child dialog:
        context.Call(FormDialog.FromForm(BuildJsonForm, FormOptions.PromptInStart), CompletedCaseSubmission);
    }
    else
    {
        // We will implement these capabilities in a future post
        // In the meantime, include a notification:
        await context.PostAsync("I will be able to help you with those things soon. Stay tuned.");
        context.Wait(MessageReceivedAsync);
    }

}

 

We are now ready to build and test our bot. As before, we press F5 in Visual Studio, which will Start Debugging. Once debugging has started, an Internet Explorer window should pop up, containing your bot endpoint information.

We also launch our v3 Bot Framework Channel Emulator which we downloaded earlier. We take the URL from the IE pop-up address bar, append /api/messages, and enter this in the URL field of the emulator. We can use the default MicrosoftAppId (blank by default) and MicrosoftAppPassword (also blank by default) from the application template Web.config file, and add them in the appropriate inputs in the emulator as well. Note that these parameter names changed from v1 to v3.

We can now have a conversation with our bot, via the emulator, and test out the submission of our case to CRM via the bot:

CaseCreateemulatorAdapted

 

The case has now been created and appears in CRM, and we can see that it originated from the bot:

CaseInCRM

 

Note that if you wish to use a custom icon for cases originating from the bot, as shown above, see here for instructions.

 

Our bot is again ready to be published to Microsoft Azure, registered with the Bot Framework, and made available across channels. If you wish to walk through the publishing of your bot and configuring of channels, you can find details here.

Here is a brief video showing our bot’s behavior when published and deployed to the Skype channel:

 

You can download the full source code for the solution here. Prior to building the sample, ensure you:

  • Populate your CRM Web API Credentials in the Web.config file
  • Populate a contactid Guid from your CRM instance in the MessageReceivedAsync method in ServiceDialog.cs
  • Populate three product Guids and product names in CreateCase.json
  • Add a new caseorigincode Option Set value of 100000001 via the Dynamics CRM Settings > Customization > Customize the System administration
Comments (2)

  1. Adam Piercy says:

    Hi you say a pre-requisite is CRM Online, does this mean CRM 2016 hosted within Azure or On-Prem won’t work?

    1. Geoff Innis says:

      Hi Adam, connecting a bot to CRM 2016 hosted in Azure or on-prem should work as well, though the specific details of setting up the authentication to the CRM Web API may differ.

Skip to main content