As Microsoft’s Copilot becomes an integral part of the Dynamics 365 CRM ecosystem, understanding how users interact with it has become essential for businesses aiming to enhance their digital customer experience. Recently, a client approached us with a unique requirement – they wanted to capture the user details like username, Business Unit, Timestamp, and the exact question they typed when interacting with Copilot within their D365 CRM environment.
In this blog, we’ll walk you through how I approached this scenario and built a Power Automate flow with an Action & Plugin that captures these interactions effectively.
Begin by creating a solution in Power Apps, or open the solution you created earlier organized and easy to manage within the Dynamics 365 environment.
Step 2: Create an Automated Cloud Flow
Step 3: Configure Trigger
Step 4: Trigger and Initialize
I used the “Copilot Interaction” table to trigger the flow and then fetched the related transcript data using the dataid.
1. Trigger:
2. Initialize Variable: (Store the Question Ask By User)
After capturing the interaction, we parse the msdyn_interactioncontext using a Compose action to access the full JSON object. From this, we extract the Transcript.DataId to use later for fetching the related transcript.
Then, we use a Condition block to validate whether the interaction context is valid and contains data.
Expressions used:
1. To check if the interaction context is not empty:
outputs(‘Interaction_Context’) is not equal to json(‘{}’)
2. To extract the Transcript.DataId from the interaction context:
outputs(‘Interaction_Context’)?[‘Transcript’]?[‘DataId’]
These expressions make sure the context is valid and allow us to move forward only if the DataId exists, preventing flow failures or unnecessary steps.
Once we have the Transcript.DataId, we use the Get a row by ID – Copilot Transcript Datas action to retrieve detailed information about the interaction from the Dataverse table.
Then, we extract and format the following fields using Compose actions:
1. Created By (User Name)
To get the formatted user name who initiated the Copilot interaction:
outputs(‘Get_a_row_by_ID_-_Copilot_Transcript_Datas’)?[‘body/_createdby_value@OData.Community.Display.V1.FormattedValue’]
2. Business Unit
To determine which business unit the user belongs to:
outputs(‘Get_a_row_by_ID_-_Copilot_Transcript_Datas’)?[‘body/_owningbusinessunit_value@OData.Community.Display.V1.FormattedValue’]
3. Timestamp (Last Modified)
This gives us the last modified time of the interaction record, representing the time of the chat:
outputs(‘Get_a_row_by_ID_-_Copilot_Transcript_Datas’)?[‘body/modifiedon@OData.Community.Display.V1.FormattedValue’]
These expressions help extract clean, readable values for display or reporting purposes
Once we have the Transcript Data (which is stored in Base64 UTF-16LE format), we need to decode it properly to extract readable JSON data.
Steps:
1. Download a Transcript Data File
2. Convert to Base64 String
3. Why decoding directly doesn’t work
The data is in UTF-16 Little Endian (UTF-16LE) format. Standard base64ToString() or decodeUriComponent() methods don’t work properly here.
➤So we need to use a Dataverse plugin to handle the decoding correctly.
4. Perform Unbound Action (Call Action & Plugin)
5. Develop a Plugin
using System; using Microsoft.Xrm.Sdk; using System.Text; namespace DecodeBase64String { public class DecodeBase64 : IPlugin { public void Execute(IServiceProvider serviceProvider) { // Declare variables outside the try block IPluginExecutionContext context = null; IOrganizationServiceFactory serviceFactory = null; IOrganizationService service = null; ITracingService tracingService = null; string inputParameterName = "Base64Input"; string outputParameterName = "ConvertedUTF8"; string base64String = string.Empty; byte[] utf16Bytes = null; byte[] utf8Bytes = null; string utf8String = string.Empty; Encoding utf16Encoding = Encoding.Unicode; // UTF-16LE Encoding utf8Encoding = Encoding.UTF8; try { // Get services context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); service = serviceFactory.CreateOrganizationService(context.UserId); tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); // Check input parameter if (!context.InputParameters.ContainsKey(inputParameterName) || context.InputParameters[inputParameterName] == null) { throw new InvalidPluginExecutionException($"Input parameter '{inputParameterName}' not found or is null."); } // Get Base64 string base64String = context.InputParameters[inputParameterName] as string; if (string.IsNullOrEmpty(base64String)) { tracingService.Trace("Base64 string is empty."); context.OutputParameters[outputParameterName] = string.Empty; return; } tracingService.Trace($"Base64 String: {base64String}"); // Decode from Base64 to UTF-16LE bytes utf16Bytes = Convert.FromBase64String(base64String); // Convert UTF-16LE bytes to UTF-8 bytes utf8Bytes = Encoding.Convert(utf16Encoding, utf8Encoding, utf16Bytes); // Convert bytes to UTF-8 string utf8String = utf8Encoding.GetString(utf8Bytes); tracingService.Trace($"UTF-8 String: {utf8String}"); // Set output context.OutputParameters[outputParameterName] = utf8String; tracingService.Trace($"Output parameter '{outputParameterName}' set with UTF-8 string."); } catch (Exception ex) { if (tracingService != null) { tracingService.Trace($"Error: {ex.Message}"); } throw new InvalidPluginExecutionException($"An error occurred in DecodeBase64 plugin: {ex.Message}", ex); } } } }
1. Add another Compose action:
json(outputs(‘Perform_an_unbound_action’)?[‘body/DecodedData’])
2. Now your data is clean, structured, and ready to be used as JSON in your Power Automate flow.
Note:
You could use third-party websites available online, but most are either paid or less secure. This plugin-based method is 100% free, secure, and directly integrated with Dataverse
Each interaction with Copilot is stored in the Copilot Transcript Datas table.
After decoding the base64 content, we now parse the JSON structure to access individual messages.
Action: Parse JSON
{ "type": "object", "properties": { "messages": { "type": "array", "items": { "type": "object", "properties": { "user": { "type": "string" }, "message": { "type": "string" } }, "required": ["user", "message"] } } } }
Apply to Each: Loop Through Messages
This iterates over every message within the parsed JSON array.
This ensures you’re only extracting the questions typed by the user (not by Copilot or bot).
Once we’ve confirmed the message is from the user, we now want to store these questions and make them available for further processing or display.
Append Question to Array Variable
Action: Compose (to get all questions as a single string)
This combines all user questions with line breaks:
join(variables(‘Questions’), decodeUriComponent(‘%0A’))
Action: Compose (to get the latest question and timestamp)
To show only the most recent question along with its timestamp:
last(variables(‘Questions’)) – @{outputs(‘Timestamp’)}
This is especially helpful if you’re showing real-time Q&A or logging the latest query.
Ask your question in Copilot (e.g., “Can u give details of mobile heating issue?”).
Go to Power Automate → My Flows → Check the latest run to see the question with the timestamp in the output.
In this blog, we used Power Automate to build a flow that captures user questions via Copilot and stores them with a timestamp. We used actions like Compose to handle and display the data clearly. This setup helps automate data collection for further processing or integration with other systems.
The post How to capture user questions asked to Copilot in Dynamics 365 CRM using Power Automate first appeared on Microsoft Dynamics 365 CRM Tips and Tricks.
Original Post https://www.inogic.com/blog/2025/04/how-to-capture-user-questions-asked-to-copilot-in-dynamics-365-crm-using-power-automate/