As you have probably noticed, Business Central is actively integrating AI and providing developer tools for working with large language models. One of these tools is the new page type PromptDialog. I suggest exploring this page type along with the new AI interfaces using simple example: create Copilot for generating Payment Terms records.
First of all, I want to note that Microsoft and OpenAI provide very detailed documentation on working with AI and PromptPages, so I will frequently refer to the documentation. It is also worth checking out the BCTech repository, specifically the AzureOpenAI samples. While writing this post, I was inspired by these examples.
So, PromptDialog page is a unique page type designed for working with generative AI (LLM) in Business Central. The page interface is specifically created to assist users in solving various tasks. A PromptDialog page can utilize a wide range of LLM models. In our example, we will use GPT-4o from OpenAI, which we will deploy on Azure using the Azure OpenAI Service. Overall, the final result can be called a Copilot, meaning an AI-powered assistant for working in Business Central.
I am convinced that to write an engaging post, it is essential to use practical and useful examples, and even better when they solve a specific problem. For this reason, I often create demo applications and upload them to my GitHub repository. This post is no exception, by the end, we will have a fully functional Copilot for creating Payment Terms.
The idea is quite simple: we need to generate Payment Terms by translating user preferences into structured data. Many end-users struggle with Date Formulas, which is not surprising given their complex syntax, especially for non-technical users. Our newly created extension, "Suggest Payment Terms with AI", will also assist in writing correct Date Formulas.
Still, we need to understand the UI structure of PromptDialog pages a bit. Since, as I already mentioned, the PromptDialog documentation is quite detailed, it makes sense to provide only the main structure without going into excessive detail.
To better understand which sections of the page are responsible for specific UI elements and functionality in a PromptPage, I will provide screenshots from a Copilot page along with a brief description of each section and the full code of the page. Additionally, I have structured the page code by using #region directive to separate different sections. Of course, the page type must be set to PromptDialog, with the mandatory property: Extensible = false.
When first opened, the Copilot page will look as shown in the screenshot below. Later, we will dive into how everything works, but for now, let's focus on the UI.
As we can see, the PromptDialog page consists of several main sections:
When selecting an action from the PromptGuide section, the page will transition to the next state, where the global text variable InputPaymentTermsDescription will be assigned one of the example prompts for the user.
After the user finishes formulating the prompt and clicks Generate, the page will update and display several new sections, specifically:
I also provide a simplified version of the page's code with a minimal structure to better understand its layout and functionality.
page 81802 "SPT SuggestPT - Proposal"
{
PageType = PromptDialog;
Extensible = false;
Caption = 'Draft new payment terms with Copilot';
DataCaptionExpression = InputPaymentTermsDescription;
IsPreview = true;
layout
{
#region input section
area(Prompt)
{
field(PaymentTermsDescriptionField; InputPaymentTermsDescription)
{
ApplicationArea = All;
ShowCaption = false;
MultiLine = true;
InstructionalText = 'Describe the payment terms you want to create with Copilot vld-bc.com';
}
}
#endregion
#region output section
area(Content)
{
part(ProposalDetails; "SPT Payment Terms ProposalSub.")
{
Caption = 'Payment Terms';
ShowFilter = false;
ApplicationArea = All;
}
}
#endregion
}
actions
{
#region prompt guide
area(PromptGuide)
{
action(NetDate)
{
ApplicationArea = All;
Caption = 'Net date';
ToolTip = 'Net date';
trigger OnAction()
begin
InputPaymentTermsDescription := 'Create payment terms where the due date is [DueDateNumberOfDays] days from the invoice date.';
end;
}
}
#endregion
#region system actions
area(SystemActions)
{
systemaction(Generate)
{
Caption = 'Generate';
ToolTip = 'Generate a payment terms with Dynamics 365 Copilot.';
trigger OnAction()
begin
RunGeneration();
end;
}
systemaction(OK)
{
Caption = 'Keep it';
ToolTip = 'Save the Payment Terms proposed by Dynamics 365 Copilot.';
}
systemaction(Cancel)
{
Caption = 'Discard it';
ToolTip = 'Discard the Payment Terms proposed by Dynamics 365 Copilot.';
}
systemaction(Regenerate)
{
Caption = 'Regenerate';
ToolTip = 'Regenerate the Payment Terms proposed by Dynamics 365 Copilot.';
trigger OnAction()
begin
RunGeneration();
end;
}
#endregion
}
}
var
InputPaymentTermsDescription: Text;
}
To make this page functional, we need to connect it to an AI model, and in our case, that is OpenAI model gpt 4o. In Business Central, there are two ways to achieve this:
Our choice is self managed AI resource. For this, we will use Azure Azure OpenAI Service.
Here is step by step guide:
1.Go to portal azure marketplace and search for Azure OpenAI
2.Create Azure OpenAI service
3.Go to Azure AI Foundry portal from created service
4.Select chat completion model gpt-4o in model catalog
5.Deploy selected model
6.Go to your deployed model from model catalog
7.Open in Playground, also here you can see your API key and model name.
8.Go to View code to see code examples. In chat playground windows we can chat with our model as well.
9.Code samples is useful if you would like to use your model via some programming language as python etc. In our case we would like to get endpoint URL and model name.
As I specified before, we are interested in three key values required to use this model in the PromptDialog page:
Before integration the AI model into the PromptDialog, we need to register our Copilot capability. To do this, we must extend the Copilot Capability enum and add our new Copilot: "Suggest Payment Terms". After that, we can create an Installation codeunit, which will automatically register "Suggest Payment Terms" during installation.
enumextension 81801 "SPT Copilot Capability" extends "Copilot Capability" //2000000003
{
value(81801; "SPT Suggest Payment Terms")
{
Caption = 'Suggest Payment Terms';
}
}
codeunit 81803 "SPT Install"
{
Subtype = Install;
InherentEntitlements = X;
InherentPermissions = X;
Access = Internal;
trigger OnInstallAppPerDatabase()
begin
RegisterCapability();
end;
local procedure RegisterCapability()
var
CopilotCapability: Codeunit "Copilot Capability";
LearnMoreUrlTxt: Label 'https://vld-bc.com/blog/ai-prompt-pages-in-bc', Locked = true;
begin
if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"SPT Suggest Payment Terms") then
CopilotCapability.RegisterCapability(Enum::"Copilot Capability"::"SPT Suggest Payment Terms", Enum::"Copilot Availability"::Preview, LearnMoreUrlTxt);
end;
}
After that, "Suggest Payment Terms" will appear in the list of available capabilities on the "Copilot & AI Capabilities" page. Note: As a best practice, it is recommended to initially position your solution as a preview. This allows time to gather user feedback and ensure the solution works effectively.
Now, let's talk a bit about the structure of requests to OpenAI GPT-4o and how to properly craft prompts. I highly recommend reading about prompt engineering best practices from OpenAI and documentation from Microsoft.
In short, a request consists of:
Of course, this is a very brief explanation and doesn't cover everything, but it's enough to get started.
To achieve a structured response from GPT-4o, we can use function calling, also read Microsoft documentation. This allows generative AI to be applied in scenarios such as executing code or interacting with external services. Essentially, it involves defining a function and its parameters so that the function can be called within a request to execute specific code. In our case, this function will be used to create Payment Terms.
So, to achieve the creation of Payment Terms, we need to define:
I suggest starting with the creation of the AI Function for generating Payment Terms. I named this function create_paymentterms and added several parameters that correspond to the fields in Business Central that I want to populate using AI. Each of these fields has a name, type, and description. Here is the final function for review:
{
"type": "function",
"function": {
"name": "create_paymentterms",
"description": "Call this function to create a new payment terms",
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "A short code for payment terms (max 10 characters)."
},
"desc": {
"type": "string",
"description": "A short description for this payment terms (max 100 characters)."
},
"dueDateCalculation": {
"type": "string",
"description": "Specifies a date formula for calculating the due date relative to the invoice date. If a fixed day-of-month is mentioned (e.g., 20th), convert it to the corresponding Business Central formula (e.g., -CM+19D). Valid examples: 3D, -CM+7D, 1W, CW+1D."
},
"discountDateCalculation": {
"type": "string",
"description": "Specifies the date formula for the discount date relative to the invoice date. If a fixed day-of-month is mentioned (e.g., 12th), convert it to the corresponding Business Central formula (e.g., -CM+11D). Otherwise, use a relative day count. Valid examples: 3D, -CM+7D, 1W, CW+1D. Empty string if not applicable."
},
"discountPercent": {
"type": "number",
"description": "Specifies the discount percent if applicable, or zero if not."
}
},
"required": [
"code",
"desc",
"dueDateCalculation",
"discountDateCalculation",
"discountPercent"
]
}
}
}
As you may have noticed, for the Date Formula fields, I provided a more detailed description, since this format is quite unique and requires a clear explanation for the AI on how to handle it. I also specified maximum lengths where necessary and allowed for empty values in certain fields where applicable.
Next, let's create the System Prompt, where we will define the context of the task and provide instructions. We will also specify that to create Payment Terms, the previously created function create_paymentterms must be called.
The user will describe Payment Terms. Your task is to prepare the payment terms with the described date formulas for Microsoft Dynamics 365 Business Central.
**Important:**
- If the user refers to a fixed day-of-month (e.g., "12th", "20th"), convert that into a valid Business Central date formula using the rule: For day n, use "-CM+(n-1)D". For instance, "12th" becomes "-CM+11D" and "20th" becomes "-CM+19D".
- If the user refers to a relative number of days (e.g., "20 days from invoice date"), use that number directly (e.g., "20D").
- Validate the computed date formulas with examples for invoice dates before and after the computed dates.
- Follow the valid Business Central syntax (e.g., 3D, -CM+7D, 1W, CW+1D).
Call the function "create_paymentterms" to create the payment terms.
The last step is to provide User Message examples for the Prompt Guide. I want to emphasize that it is very important to include a guide for users so they understand how to properly formulate prompts. I suggest to read about it. Here is some examples of prompt guides for suggesting Payment Terms:
Create payment terms where the due date is [DueDateNumberOfDays] days from the invoice date.
Create payment terms where the due date is [DueDateNumberOfDays] days after end of current month from the invoice date.
Generate Payment Terms for every [DiscountDateDayOfMonth]th of the next month with a payment discount of [PaymentDiscountPercent]% and due date on the [DueDateDayOfMonth]th of the next month.
Now we are ready for the practical implementation of our request to the AI model in Business Central using AL. To facilitate this, Microsoft has introduced several interfaces for working with OpenAI models. For example, to implement an AI function, there is the AOAI Function interface. Here is an example of its implementation:
codeunit 81802 "SPT Create Payment Terms" implements "AOAI Function"
{
var
FunctionNameLbl: Label 'create_paymentterms', Locked = true;
procedure GetPrompt(): JsonObject
var
ToolDefinition: JsonObject;
FunctionDefinition: JsonObject;
ParametersDefinition: JsonObject;
begin
ParametersDefinition.ReadFrom(
'{"type": "object",' +
'"properties": {' +
'"code": { "type": "string", "description": "A short code for payment terms (max 10 characters)."},' +
'"desc": { "type": "string", "description": "A short description for this payment terms (max 100 characters)."},' +
'"dueDateCalculation": { "type": "string", "description": "Specifies a date formula for calculating the due date relative to the invoice date. If a fixed day-of-month is mentioned (e.g., 20th), convert it to the corresponding Business Central formula (e.g., -CM+19D). Valid examples: 3D, -CM+7D, 1W, CW+1D."},' +
'"discountDateCalculation": { "type": "string", "description": "Specifies the date formula for the discount date relative to the invoice date. If a fixed day-of-month is mentioned (e.g., 12th), convert it to the corresponding Business Central formula (e.g., -CM+11D). Otherwise, use a relative day count. Valid examples: 3D, -CM+7D, 1W, CW+1D. Empty string if not applicable."},' +
'"discountPercent": { "type": "number", "description": "Specifies the discount percent if applicable, or zero if not."}' +
'},"required": ["code", "desc", "dueDateCalculation", "discountDateCalculation", "discountPercent"]}'
);
FunctionDefinition.Add('name', FunctionNameLbl);
FunctionDefinition.Add('description', 'Call this function to create a new payment terms');
FunctionDefinition.Add('parameters', ParametersDefinition);
ToolDefinition.Add('type', 'function');
ToolDefinition.Add('function', FunctionDefinition);
exit(ToolDefinition);
end;
procedure Execute(Arguments: JsonObject): Variant
var
Code, Description, DueDateCalculation, DiscountDateCalculation, DiscountPercent : JsonToken;
begin
Arguments.Get('code', Code);
Arguments.Get('desc', Description);
Arguments.Get('dueDateCalculation', DueDateCalculation);
Arguments.Get('discountDateCalculation', DiscountDateCalculation);
Arguments.Get('discountPercent', DiscountPercent);
TempPaymentTerms.Init();
TempPaymentTerms.Code := Code.AsValue().AsCode();
TempPaymentTerms.Description := Description.AsValue().AsText();
Evaluate(TempPaymentTerms."Due Date Calculation", DueDateCalculation.AsValue().AsText());
if DiscountDateCalculation.AsValue().AsText() <> '' then
Evaluate(TempPaymentTerms."Discount Date Calculation", DiscountDateCalculation.AsValue().AsText());
TempPaymentTerms."Discount %" := DiscountPercent.AsValue().AsDecimal();
TempPaymentTerms.Insert();
exit('Completed creating payment terms');
end;
procedure GetName(): Text
begin
exit(FunctionNameLbl);
end;
procedure GetPaymentTerms(var LocalPaymentTerms: Record "Payment Terms" temporary)
begin
LocalPaymentTerms.Copy(TempPaymentTerms, true);
end;
var
TempPaymentTerms: Record "Payment Terms" temporary;
}
The GetPrompt() function essentially creates the structure of the AI function, while the Execute() function processes the returned result in the format defined by the AI function. As you can see in Execute() we actually generate temporary record Payment Terms as output for user.
Now, we just need to authenticate, pass the AI function, System Prompt, and User Prompt. After that, we can retrieve the result of the AI function execution using the GetPaymentTerms method in our implementation of the AOAI Function interface.
local procedure GeneratePaymentTermProposal()
var
SuggestPaymentTermsSetup: Record "SPT PT AI Setup";
AzureOpenAI: Codeunit "Azure OpenAI";
AOAIOperationResponse: Codeunit "AOAI Operation Response";
AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params";
AOAIChatMessages: Codeunit "AOAI Chat Messages";
SuggestPaymentTerms: Codeunit "SPT Create Payment Terms";
begin
SuggestPaymentTermsSetup.Get();
SuggestPaymentTermsSetup.TestField("Endpoint URL");
SuggestPaymentTermsSetup.TestField("Model Name");
AzureOpenAI.SetAuthorization(Enum::"AOAI Model Type"::"Chat Completions",
SuggestPaymentTermsSetup."Endpoint URL", SuggestPaymentTermsSetup."Model Name", SuggestPaymentTermsSetup.GetSecret());
AzureOpenAI.SetCopilotCapability(Enum::"Copilot Capability"::"SPT Suggest Payment Terms");
AOAIChatCompletionParams.SetMaxTokens(2500);
AOAIChatCompletionParams.SetTemperature(0);
AOAIChatMessages.AddSystemMessage(GetSystemPrompt());
AOAIChatMessages.AddUserMessage(UserPrompt);
AOAIChatMessages.AddTool(SuggestPaymentTerms);
AOAIChatMessages.SetToolInvokePreference("AOAI Tool Invoke Preference"::Automatic);
AOAIChatMessages.SetToolChoice('auto');
AzureOpenAI.GenerateChatCompletion(AOAIChatMessages, AOAIChatCompletionParams, AOAIOperationResponse);
if not AOAIOperationResponse.IsSuccess() then
Error(AOAIOperationResponse.GetError());
SuggestPaymentTerms.GetPaymentTerms(TempPaymentTerms);
end;
Some interesting parameters used in the implementation are worth noting:
The remaining question is: how will users access our new Copilot – "Suggest Payment Terms"? To enable this, we simply need to add our page as an action in the Prompting category.
pageextension 81801 "SPT Payment Terms" extends "Payment Terms" //4
{
actions
{
addfirst(Category_New)
{
actionref(SPTGenerateCopilotPromoted; SPTGenerateCopilotAction)
{
}
}
addlast(Prompting)
{
action(SPTGenerateCopilotAction)
{
Caption = 'Draft with Copilot';
Ellipsis = true;
ApplicationArea = All;
ToolTip = 'Lets Copilot generate a draft payment terms based on your description.';
Image = Sparkle;
trigger OnAction()
begin
Page.RunModal(Page::"SPT SuggestPT - Proposal");
end;
}
}
}
}
This will add special AI action "Draft with Copilot" to the page we want, in our case it's Payment Terms.
To get started, go to the "Suggest Payment Terms Setup" page and fill in the information we previously obtained from Azure OpenAI Service: Model name, Endpoint URL, API Key.
Access to the newly created Copilot is available on the Payment Terms page.