What is Copilot Toolkit
At Directions EMEA 2023, Microsoft announced the Copilot Toolkit for Business Central.
This toolkit is a combination of UI components and an AL framework designed to help developers easily integrate AI functionalities into their Business Central extensions.
Composition of the Copilot Toolkit:
- Prompt Dialog Page (UI)
- AI module
- AI administration
To get hands-on with the toolkit quickly, follow the links to:
Find already implemented copilots in Business Central 2023 Wave 2’s features such as:
For developers aiming to embed AI into their extensions, the workflow would involve:
- Extending Copilot Capability with Custom Copilot
- Registering Custom Copilot in the Copilot & AI Capabilities
- Developing Prompt Dialog Page
- Developing AI logic, using prompt engineering and AI module.
- Calling the Prompt Dialog Page from the Business Central Page
This blog will focus on the UI aspect – the Prompt Dialog Page. I will address other components, including the AI module in the next blog.
What is Prompt Dialog Page
A picture is worth a thousand words.

What you see in the video are three stages of the generation process:
- Input Screen
- Processing Screen
- Result Screen
In Business Central 23.1 (yes, a minor update is required), this is accomplished through a PromptDialog
type page. Here’s an example of what the code might look like:
page 50103 "GPT Copilot Proposal"
{
Caption = 'Generate with Copilot';
PageType = PromptDialog;
IsPreview = true;
Extensible = false;
ApplicationArea = All;
layout
{
area(Prompt)
{
field(InputText; InputText)
{
ShowCaption = false;
MultiLine = true;
ApplicationArea = All;
}
}
area(Content)
{
field(GeneratedText; GeneratedText)
{
ShowCaption = false;
MultiLine = true;
ApplicationArea = All;
}
}
}
actions
{
area(SystemActions)
{
systemaction(Generate)
{
trigger OnAction()
begin
GeneratedText := 'You asked me about: ' + InputText;
end;
}
systemaction(Cancel)
{
ToolTip = 'Discards all suggestions and dismisses the dialog';
}
systemaction(Ok)
{
Caption = 'Keep it';
ToolTip = 'Accepts the current suggestion and dismisses the dialog';
}
}
}
var
InputText: Text;
GeneratedText: Text;
}
The code defines three screens corresponding to specific area types within the layout:
- Input Screen corresponds to the
Prompt
area, where users enter their information. - Processing Screen doesn’t correspond to any specific area, as it’s a transitional phase.
- Result Screen matches the
Content
area, where the outcomes are displayed.
Essentially, the layout determines what is displayed on each screen.
How platform understand what screen to show up?
The PromptDialog has three modes:
- Prompt Mode: For user input.
- Generate Mode: For processing the input.
- Content Mode: For displaying results.
Connecting these modes to our earlier logic:
- The Input Screen uses the Prompt Mode, which is associated with the Prompt area.
- The Processing Screen operates in Generate Mode, which does not have a specific area.
- The Result Screen is in Content Mode, linked to the Content area.
Additionally, if you want the PromptDialog page to appear only in one specific mode, you can set this preference using the PromptMode
property.
PromptMode = Content;
This approach is ideal when input is unnecessary, and only the result needs to be shown.
Using prompt modes platform understands what controls to show on the screen
How platform understand what mode to activate?
By default, the mode is set to Prompt
. To switch from the Prompt
mode to the Generate
mode you need to add a systemaction
of type Generate
actions
{
area(SystemActions)
{
systemaction(Generate)
{
trigger OnAction()
begin
// Your generative code goes here
end;
}
}
}

The platform transitions from Generate
to Content
mode upon completion of the code in the OnAction
trigger.
Why do you need Content mode?
The essence of the Copilot toolkit is to keep the user in command.
Whenever AI generates content, there’s a chance it might not be completely accurate, so it’s crucial that the user reviews and either approves or rejects the outcome. The content should only be saved to the database after the user has given their consent.
This is the purpose of Content mode. It exists to give the user the option to either confirm the information or exit the page without saving any changes. For this process, you should incorporate two system actions: Ok
to confirm and Cancel
to exit without saving.
This is also the reason why you cannot have a physical SourceTable in a PromptDialog page (you either use a temporary table, or no table)
systemaction(Cancel)
{
ToolTip = 'Discards all suggestions and dismisses the dialog';
}
systemaction(Ok)
{
Caption = 'Keep it';
ToolTip = 'Accepts the current suggestion and dismisses the dialog';
}

Enhancing the User Experience
For a more user-friendly experience, you can add an additional Regenerate
system action in Content mode:
systemaction(Regenerate)
{
trigger OnAction()
begin
GeneratedText := 'Your input was: ' + InputText + ' and I regenerated it';
end;
}

Saving Results
Attempting to “save to database” within the OnAction
trigger of the Ok
system action will result in an error

Instead, you should place this code in the OnQueryClosePage
trigger.
trigger OnQueryClosePage(CloseAction: Action): Boolean
begin
if CloseAction = CloseAction::Ok then begin
// Here you can do something with the generated text and save result to a database
Message('You accepted the generated text: ' + GeneratedText);
end;
end;
Using Captions on SystemActions
Here’s important update from Microsoft regarding the use of captions for SystemActions:
It is now recommended to always provide a caption for system actions. We now display both the icon and caption text for these system actions in the footer of the prompt dialog. Even if the caption may not be visible in the dialog, it's crucial to include one for accessibility purposes, ensuring that tools like screen readers can provide assistance for all actions.
Calling the PromptDialog Page
Nothing special here, you call it as a normal page in a RunModal
mode. The only difference is that action image should be Sparkle
pageextension 50100 "GPT Customer card Ext" extends "Customer Card"
{
actions
{
addfirst("&Customer")
{
action("GPT Happy Customer")
{
Caption = 'Make Customer Happy!';
ToolTip = 'Make Customer Happy with Copilot';
ApplicationArea = All;
Image = Sparkle;
trigger OnAction()
begin
Page.RunModal(Page::"GPT Copilot Proposal");
end;
}
}
}
}
Or, if you need to pass some input variables to the PromptDialog page
page 50103 "GPT Copilot Proposal"
{
...
procedure SetCustomer(Customer: Record Customer)
begin
InputText := 'Make ' + Customer.Name + ' happy';
end;
}
pageextension 50100 "GPT Customer card Ext" extends "Customer Card"
{
actions
{
addfirst("&Customer")
{
action("GPT Happy Customer")
{
Caption = 'Make Customer Happy!';
ToolTip = 'Make Customer Happy with Copilot';
ApplicationArea = All;
Image = Sparkle;
trigger OnAction()
var
CopilotProposal: Page "GPT Copilot Proposal";
begin
CopilotProposal.SetCustomer(Rec);
CopilotProposal.RunModal;
end;
}
}
addfirst(Promoted)
{
actionref("GPTHappyCustomer_Promoted"; "GPT Happy Customer") { }
}
}
}

Understanding When to Use Sparkle and SparkleFilled Icons
Here’s important update from Microsoft regarding the use of Sparkle icon:

Regarding the new sparkle image icon, there are two versions available: Sparkle and SparkleFilled. To clarify when to use each: 1. By default, use the Sparkle image for all Copilot actions. 2. If there are multiple Copilot actions in an area, and one of them is considered more "important" or needs to be emphasized, then use the SparkleFilled icon. Typically, there will be zero or one "filled" action, while the rest will use the normal Sparkle icon.
Beyond the Basics
The methods outlined represent the foundational elements to keep in mind. However, there are extra controls at your disposal that can simplify and enhance the management of the generative process.
Structured or Non-structured controls
Prompt and Content modes can have not only free text UI. Depending on your task, you can add fields or even pages.
You can’t use repeater in the PromptDialog page.
To add a list page, use part
.
page 50103 "GPT Copilot Proposal"
{
...
SourceTable = Customer;
SourceTableTemporary = true;
layout
{
area(Prompt)
{
field(CustomerName; Rec.Name)
{
Caption = 'Name';
ApplicationArea = All;
}
field(CustomerBalance; Rec.Balance)
{
Caption = 'Balance';
ApplicationArea = All;
}
part(SupPartInvoices; "GPT Posted Sales Invoices Part")
{
SubPageLink = "Sell-to Customer No." = field("No.");
}
...
}
...
}
...
}
...
procedure SetCustomer(Customer: Record Customer)
begin
...
Rec := Customer;
Rec.Insert();
end;
}
page 50104 "GPT Posted Sales Invoices Part"
{
PageType = ListPart;
SourceTable = "Sales Invoice Header";
Editable = false;
Caption = 'Invoices';
layout
{
area(Content)
{
repeater(Invoices)
{
field("No."; Rec."No.")
{
ApplicationArea = All;
}
field("Posting Date"; Rec."Posting Date")
{
ApplicationArea = All;
}
field(Amount; Rec.Amount)
{
ApplicationArea = All;
}
}
}
}
}

Upload attachments
There is a systemaction of type Attach
that is considered to show up in the Prompt
mode. The idea is to upload any document, that will be required during the AI Generation process.
However, if you don’t require such feature, I found this to be useful place to call the Custom Copilot Setup page.
systemaction(Attach)
{
Caption = 'Setup';
trigger OnAction()
begin
Page.Run(Page::"GPT Copilot Setup");
end;
}

Custom progress indicator message
You can substitute the default ‘Generating with Copilot’
systemaction(Generate)
{
trigger OnAction()
var
ProgressDialog: Dialog;
begin
ProgressDialog.Open('Making Customer Happy...');
// Here you can call your Copilot API
ProgressDialog.Close();
end;
}
Custom page caption
You can update the page caption. This is useful, to show the user initial prompt after result was generated
page 50103 "GPT Copilot Proposal"
{
...
DataCaptionExpression = InputText;
...
}

Adding Prompt Parameters
You can also incorporate further customizable options to guide the AI generation process, such as tone, style, context, length, and specificity, allowing for more tailored and precise outputs.
Use area(PromptOptions)
for this. Here you can add only enum or option fields.
page 50103 "GPT Copilot Proposal"
{
...
layout
{
area(PromptOptions)
{
field(HappinessLevel; HappinessLevel)
{
ApplicationArea = All;
Caption = 'How happy should the customer be?';
}
}
...
}
var
...
HappinessLevel: Enum "GPT Customer Happiness Level";
...
}
enum 50100 "GPT Customer Happiness Level"
{
value(0; "Very Happy") { }
value(1; "Happy") { }
value(2; "Neutral") { }
value(3; "Unhappy") { }
value(4; "Very Unhappy") { }
}

Prompt options will also appear on the Content mode, so you can regenerate the result with different options.
Adding Generation History
This is interesting feature. To manage and review AI-generated content later, it’s add a SourceTable to the PromptDialog page. This table should be set as a temporary. It will capture and store the outcomes after each generation cycle, enabling you to save, track, and access previous AI outputs as needed.
Also, temporary table can save prompt parameters, so on each iteration you can see what parameters where used.
table 50102 "GPT Happy Customer"
{
TableType = Temporary;
fields
{
field(1; "Entry No."; Integer)
{
}
field(2; "Customer No."; Code[20])
{
}
field(3; "Happiness Level"; enum "GPT Customer Happiness Level")
{
}
field(4; "Generated Text"; text[250])
{
}
}
keys
{
key(PK; "Entry No.")
{
Clustered = true;
}
}
}
Now, add this table as a source and adjust logic a bit
page 50103 "GPT Copilot Proposal"
{
...
SourceTable = "GPT Happy Customer";
SourceTableTemporary = true;
layout
{
...
area(Content)
{
field(GeneratedText; Rec."Generated Text")
...
}
}
actions
{
area(SystemActions)
{
systemaction(Generate)
{
trigger OnAction()
begin
Rec."Generated Text" := GenerateText();
SaveToHistory();
end;
}
...
systemaction(Regenerate)
{
trigger OnAction()
begin
Rec."Generated Text" := GenerateText();
SaveToHistory();
end;
}
}
}
...
trigger OnAfterGetCurrRecord()
begin
HappinessLevel := Rec."Happiness Level";
end;
...
local procedure GenerateText(): Text
begin
exit('You asked me to ' + InputText + ' and I did it with the level of happiness ' + Format(HappinessLevel));
end;
local procedure SaveToHistory()
begin
Rec."Entry No." := Rec.Count + 1;
Rec."Happiness Level" := HappinessLevel;
Rec.Insert();
end;
}

One more thing
As of the time this post was written, there is an bug.
Occasionally, when you define field variables in the areas (such as PromptOptions, Prompt), the platform may not retain the values during the transition from one mode to another.
Microsoft has been notified of this issue and is actively working on a resolution.
In the meantime, to address this problem, you can implement an empty OnValidate
trigger.
So, instead of
area(Prompt)
{
field(InputText; InputText)
{
ShowCaption = false;
MultiLine = true;
ApplicationArea = All;
}
}
do this
area(Prompt)
{
field(InputText; InputText)
{
ShowCaption = false;
MultiLine = true;
ApplicationArea = All;
trigger OnValidate()
begin
end;
}
}
What's next?
Now you know how to craft great UI for the generative AI experience. The next step is to make AI experience itself.
In the next blog I will dive into the new AI module. Stay tuned.