These days, there has been too much hype about AI, especially in Microsoft ecosystem, since they launched a copilot for almost every product out there. Now, some of these copilots are still in preview phase. Some of them are helpful, while the others, not much. I won’t go into details about copilots within Microsoft ecosystem, but I will talk about using AI to complement the knowledge that one already has.
My favorite quote regarding AI is.
AI won’t replace you but someone who knows how to use AI, will
Now, I don’t know the origins of this statement, but I completely agree with it. I have made a habit of frequently using Microsoft Copilot and Open AI’s ChatGPT help for initial designing and as well as coding experiences. I frequently move around between SQL, DAX, Power Query, Power Fx and have been testing how helpful these AI agents can be.
In my experience, there are two important things that can benefit you while getting help from these AI agents.
- You can accurately describe the scenario that you want to achieve with as much details as possible.
- You know the tool/programming language to a certain extent, that you are seeking help for.
The more context an AI agent has, it is easier for it to design the solution as per your requirements and with already some knowledge of the product, you can optimize the provided code, decide, if it is correct or modify the bugs that might exist.
While preparing for PL-400 exam (Power Platform Developer), I came across plug-ins which can be used to execute logic synchronously and on various events. For people who are very much used to the Low Code/No Code nature of Power Platform, the way plug-ins are created, might be scary, however, I found them a very powerful tool to master.
Microsoft defines plug-ins as following:
A plug-in is a custom event handler that executes in response to a specific event raised during processing of a Microsoft Dataverse data operation. The plug-in is implemented as a custom class compiled into a .NET Framework assembly that can be uploaded and registered with Dataverse. One or more compiled plug-in classes within an assembly can be registered on specific events (steps) within the Dataverse event framework. When the target event occurs during a data processing operation, the code within the registered plug-in class executes providing a means to augment or modify the default data processing behaviour of the platform.
You can read more about plug-ins here: Use plug-ins to extend business processes (Microsoft Dataverse) – Power Apps | Microsoft Learn
Initial Prompt
In this article, I would describe how I used ChatGPT to help me write a plug-in for an internal project that I have been working for in Bofor. I had following use case.
Don’t allow users to create an entry for today, if the meeting invites for today have already been sent. The meeting invites time (pairing time) comes from a different table. I started with the following prompt.

Following code was generated in response to this question
using System;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
public class ValidatePairingTime : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Obtain the execution context
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
// Check if the context is for Create event
if (context.MessageName != "Create" || !context.InputParameters.Contains("Target") || !(context.InputParameters["Target"] is Entity))
return;
// Target entity
Entity targetEntity = (Entity)context.InputParameters["Target"];
if (targetEntity.LogicalName != "bfr_pairxxxxstatus")
return;
// Service and Tracing
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
try
{
// Get the date from the target entity
DateTime bfrDate = targetEntity.GetAttributeValue<DateTime>("bfr_date").ToLocalTime();
// Check if the date is today
if (bfrDate.Date != DateTime.Today)
return;
// Retrieve the single row from the bfr_pairxxxxparameters table
QueryExpression query = new QueryExpression("bfr_pairxxxxparameters")
{
ColumnSet = new ColumnSet("bfr_pairingtime")
};
Entity parameterRecord = service.RetrieveMultiple(query).Entities.FirstOrDefault();
if (parameterRecord == null)
throw new InvalidPluginExecutionException("Pairing parameters not found.");
// Get the pairing time
TimeSpan pairingTime = parameterRecord.GetAttributeValue<DateTime>("bfr_pairingtime").ToLocalTime().TimeOfDay;
// Compare pairing time with the record creation time
TimeSpan currentTime = DateTime.Now.TimeOfDay;
if (pairingTime < currentTime)
throw new InvalidPluginExecutionException("Record cannot be created after the specified pairing time.");
}
catch (Exception ex)
{
tracingService.Trace("Error in ValidatePairingTime plugin: {0}", ex.ToString());
throw new InvalidPluginExecutionException("An error occurred in the pairing time validation plugin: " + ex.Message);
}
}
}
Providing More Context
Now, I forgot to remember to provide an important information regarding the time format. Since I read through code and understood that my code might break. It is very easy to ask ChatGPT to update and you don’t need to provide the entire context again. I provided the following prompt


ChatGPT understood that the data is stored as a string and modified the code accordingly.
using System;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
public class ValidatePairingTime : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Obtain the execution context
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
// Check if the context is for Create event
if (context.MessageName != "Create" || !context.InputParameters.Contains("Target") || !(context.InputParameters["Target"] is Entity))
return;
// Target entity
Entity targetEntity = (Entity)context.InputParameters["Target"];
if (targetEntity.LogicalName != "bfr_pairxxxxstatus")
return;
// Service and Tracing
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
try
{
// Get the date from the target entity
DateTime bfrDate = targetEntity.GetAttributeValue<DateTime>("bfr_date").ToLocalTime();
// Check if the date is today
if (bfrDate.Date != DateTime.Today)
return;
// Retrieve the single row from the bfr_pairxxxxparameters table
QueryExpression query = new QueryExpression("bfr_pairxxxxparameters")
{
ColumnSet = new ColumnSet("bfr_pairingtime")
};
Entity parameterRecord = service.RetrieveMultiple(query).Entities.FirstOrDefault();
if (parameterRecord == null)
throw new InvalidPluginExecutionException("Pairing parameters not found.");
// Get the pairing time as a string
string pairingTimeString = parameterRecord.GetAttributeValue<string>("bfr_pairingtime");
if (string.IsNullOrWhiteSpace(pairingTimeString))
throw new InvalidPluginExecutionException("Pairing time is not set.");
// Parse the pairing time into a TimeSpan
if (!TimeSpan.TryParseExact(pairingTimeString, "hh\\:mm", null, out TimeSpan pairingTime))
throw new InvalidPluginExecutionException("Invalid pairing time format.");
// Compare pairing time with the record creation time
TimeSpan currentTime = DateTime.Now.TimeOfDay;
if (currentTime > pairingTime)
throw new InvalidPluginExecutionException("Record cannot be created after the specified pairing time.");
}
catch (Exception ex)
{
tracingService.Trace("Error in ValidatePairingTime plugin: {0}", ex.ToString());
throw new InvalidPluginExecutionException("An error occurred in the pairing time validation plugin: " + ex.Message);
}
}
}
Asking to Remove Code Sections
Now, if you are familiar with registering plug-ins, the execution event (create, edit, update) and stage (pre-validation, pre-operation, post-operations) are set through the plugin registration tool, so they should not be a part of the code. Hence another update


using System;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
public class ValidatePairingTime : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Obtain the execution context
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
// Target entity
Entity targetEntity = (Entity)context.InputParameters["Target"];
// Service and Tracing
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
try
{
// Get the date from the target entity
DateTime bfrDate = targetEntity.GetAttributeValue<DateTime>("bfr_date").ToLocalTime();
// Check if the date is today
if (bfrDate.Date != DateTime.Today)
return;
// Retrieve the single row from the bfr_pairxxxxparameters table
QueryExpression query = new QueryExpression("bfr_pairxxxxparameters")
{
ColumnSet = new ColumnSet("bfr_pairingtime")
};
Entity parameterRecord = service.RetrieveMultiple(query).Entities.FirstOrDefault();
if (parameterRecord == null)
throw new InvalidPluginExecutionException("Pairing parameters not found.");
// Get the pairing time as a string
string pairingTimeString = parameterRecord.GetAttributeValue<string>("bfr_pairingtime");
if (string.IsNullOrWhiteSpace(pairingTimeString))
throw new InvalidPluginExecutionException("Pairing time is not set.");
// Parse the pairing time into a TimeSpan
if (!TimeSpan.TryParseExact(pairingTimeString, "hh\\:mm", null, out TimeSpan pairingTime))
throw new InvalidPluginExecutionException("Invalid pairing time format.");
// Compare pairing time with the record creation time
TimeSpan currentTime = DateTime.Now.TimeOfDay;
if (currentTime > pairingTime)
throw new InvalidPluginExecutionException("Record cannot be created after the specified pairing time.");
}
catch (Exception ex)
{
tracingService.Trace("Error in ValidatePairingTime plugin: {0}", ex.ToString());
throw new InvalidPluginExecutionException("An error occurred in the pairing time validation plugin: " + ex.Message);
}
}
}
Solving Test Case
Now this code seemed to fulfil my requirements, I provided ChatGPT with an example data and asked it to use it to go through line by line and provide me on how the data would be processed. This is a very good technique which can help understand better the code and see for potential bugs.


Step-by-Step Execution
1. Plugin Triggered
- Input Entity: bfr_pairxxxxstatus (Target).
- bfr_date in the triggered record: 2024-11-16 00:00:00 (Today).
- Current system time: 14:45 (local time).
2. Retrieve bfr_date
- The plugin extracts the bfr_date from the target entity.
DateTime bfrDate = targetEntity.GetAttributeValue<DateTime>("bfr_date").ToLocalTime();
- Extracted Value: 2024-11-16 00:00:00 (Converted to local time, if needed).
- Condition:
if (bfrDate.Date != DateTime.Today)
return;
The condition checks if the date matches today.
- Result: bfrDate.Date == DateTime.Today → True. The plugin continues execution.
3. Retrieve Row from bfr_pairxxxxparameters
- Query to retrieve the single row from bfr_pairxxxxparameters:
QueryExpression query = new QueryExpression("bfr_pairxxxxparameters")
{
ColumnSet = new ColumnSet("bfr_pairingtime")
};
Entity parameterRecord = service.RetrieveMultiple(query).Entities.FirstOrDefault();
- Retrieved Row:
- bfr_pairingtime: “15:20” (string).
- Validation:
if (parameterRecord == null)
throw new InvalidPluginExecutionException("Pairing parameters not found.");
- Result: Row is found, plugin continues.
4. Parse bfr_pairingtime
- Parsing the bfr_pairingtime value:
string pairingTimeString = parameterRecord.GetAttributeValue<string>("bfr_pairingtime");
if (!TimeSpan.TryParseExact(pairingTimeString, "hh\\:mm", null, out TimeSpan pairingTime))
throw new InvalidPluginExecutionException("Invalid pairing time format.");
- Input Value: “15:20”.
- Parsed Value: TimeSpan pairingTime = 15:20:00.
- Validation:
if (string.IsNullOrWhiteSpace(pairingTimeString))
throw new InvalidPluginExecutionException("Pairing time is not set.");
- Result: Valid time, plugin continues.
5. Compare pairingTime with Current Time
- Get the current system time:
TimeSpan currentTime = DateTime.Now.TimeOfDay;
- Current Time: 14:45:00 (as a TimeSpan).
- Compare currentTime with pairingTime:
if (currentTime > pairingTime)
throw new InvalidPluginExecutionException("Record cannot be created after the specified pairing time.");
- Condition: 14:45:00 > 15:20:00 → False.
- Result: The condition is false, so the plugin allows the record creation to proceed.
Final Output
- Validation Result: The record passes all checks successfully.
- Plugin Outcome: No exceptions are thrown, and the record is created in the bfr_pairxxxxstatus table.
Alternative Scenario
If the current time was 15:30:
- The comparison currentTime > pairingTime would evaluate to True.
- Result: An InvalidPluginExecutionException is thrown with the message:
“Record cannot be created after the specified pairing time.”
Testing in Real Environment and Coming back
I was satisfied with this code and decided to test it in my Power Platform environment. However, the plug-in did not work, it still allowed me to create new entries for current day, after the pairing time. I thought there might be some delay before the plug-ins starts working but waiting a few minutes also did not work. I went through the entire code once again and figured out that the issue might exist in the Time Zone. As per my understanding the plugin was using the current time from UTC, whereas I am working in Finland Time zone and the time in Pairing column is as per Finnish Time.
Hence, I went back to ChatGPT, with another modification request as follows:


Conclusion
I testes the updated code on my environment and it seemed to work as desired. It was a good learning experience for me, and I got a chance to implement my first ever plug-in as well. I hope you had a good read. Following is the final working code.
using System;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
public class ValidatePairingTime : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Obtain the execution context
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
// Target entity
Entity targetEntity = (Entity)context.InputParameters["Target"];
// Service and Tracing
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
try
{
// Get the date from the target entity
DateTime bfrDate = targetEntity.GetAttributeValue<DateTime>("bfr_date").ToLocalTime();
// Check if the date is today
if (bfrDate.Date != DateTime.Today)
return;
// Retrieve the single row from the bfr_pairxxxxparameters table
QueryExpression query = new QueryExpression("bfr_pairxxxxparameters")
{
ColumnSet = new ColumnSet("bfr_pairingtime")
};
Entity parameterRecord = service.RetrieveMultiple(query).Entities.FirstOrDefault();
if (parameterRecord == null)
throw new InvalidPluginExecutionException("Pairing parameters not found.");
// Get the pairing time as a string
string pairingTimeString = parameterRecord.GetAttributeValue<string>("bfr_pairingtime");
if (string.IsNullOrWhiteSpace(pairingTimeString))
throw new InvalidPluginExecutionException("Pairing time is not set.");
// Parse the pairing time into a TimeSpan
if (!TimeSpan.TryParseExact(pairingTimeString, "hh\\:mm", null, out TimeSpan pairingTime))
throw new InvalidPluginExecutionException("Invalid pairing time format.");
// Convert current UTC time to Finland time
DateTime utcNow = DateTime.UtcNow;
TimeZoneInfo finlandTimeZone = TimeZoneInfo.FindSystemTimeZoneById("FLE Standard Time"); // Finland Time Zone
DateTime finlandTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, finlandTimeZone);
// Get current time in Finland as TimeSpan
TimeSpan currentTime = finlandTime.TimeOfDay;
// Compare pairing time with the current Finland time
if (currentTime > pairingTime)
throw new InvalidPluginExecutionException("Record cannot be created after the specified pairing time.");
}
catch (Exception ex)
{
tracingService.Trace("Error in ValidatePairingTime plugin: {0}", ex.ToString());
throw new InvalidPluginExecutionException("An error occurred in the pairing time validation plugin: " + ex.Message);
}
}
}