Skill: AL Copilot Development (Full Lifecycle)
Purpose
Build AI-powered Copilot experiences in Business Central end-to-end: capability registration, PromptDialog page design, Azure OpenAI generation codeunit, and testing with AI Test Toolkit.
When to Load
This skill should be loaded when:
- A new Copilot/AI feature is being designed or implemented
- A PromptDialog page needs to be created or modified
- Azure OpenAI integration is required (chat completions, JSON mode)
- AI Test Toolkit tests need to be created for a Copilot feature
- Prompt engineering guidance is needed (system/user prompt design)
- A capability needs to be registered in BC's Copilot admin page
Phase 1: Capability Registration
Objects Required
- Enum Extension — extend
"Copilot Capability"to register your feature - Install Codeunit — register the capability on app install
- Isolated Storage Wrapper — manage Azure OpenAI secrets securely
Pattern: Enum Extension
alnamespace Contoso.CopilotFeatures; using System.AI; enumextension 50100 "Contoso Copilot Capabilities" extends "Copilot Capability" { value(50100; "Sales Forecasting") { Caption = 'Sales Forecasting with Copilot'; } }
Pattern: Install Codeunit
alnamespace Contoso.CopilotFeatures; using System.AI; codeunit 50100 "Contoso Copilot Setup" { Subtype = Install; InherentEntitlements = X; InherentPermissions = X; Access = Internal; trigger OnInstallAppPerDatabase() begin RegisterCapability(); end; local procedure RegisterCapability() var CopilotCapability: Codeunit "Copilot Capability"; LearnMoreUrlTxt: Label 'https://learn.microsoft.com/dynamics365/business-central/', Locked = true; begin if not CopilotCapability.IsCapabilityRegistered( Enum::"Copilot Capability"::"Sales Forecasting") then CopilotCapability.RegisterCapability( Enum::"Copilot Capability"::"Sales Forecasting", Enum::"Copilot Availability"::Preview, LearnMoreUrlTxt); end; }
Availability options: Preview (opt-in), GA (general availability).
After publishing, verify the capability appears in BC: search "Copilot & AI Capabilities" page.
Pattern: Isolated Storage Wrapper (Secrets)
alcodeunit 50101 "Contoso Isolated Storage" { Access = Internal; procedure GetSecretKey(): SecretText var Secret: Text; begin if IsolatedStorage.Get('AzureOpenAIKey', DataScope::Module, Secret) then exit(Secret); Error('Azure OpenAI key not configured.'); end; procedure SetSecretKey(NewKey: SecretText) begin IsolatedStorage.Set('AzureOpenAIKey', NewKey, DataScope::Module); end; procedure GetEndpoint(): Text var Endpoint: Text; begin if IsolatedStorage.Get('AzureOpenAIEndpoint', DataScope::Module, Endpoint) then exit(Endpoint); Error('Azure OpenAI endpoint not configured.'); end; procedure SetEndpoint(NewEndpoint: Text) begin IsolatedStorage.Set('AzureOpenAIEndpoint', NewEndpoint, DataScope::Module); end; procedure GetDeployment(): Text var Deployment: Text; begin if IsolatedStorage.Get('AzureOpenAIDeployment', DataScope::Module, Deployment) then exit(Deployment); exit('gpt-4o'); // default model end; procedure SetDeployment(NewDeployment: Text) begin IsolatedStorage.Set('AzureOpenAIDeployment', NewDeployment, DataScope::Module); end; }
Production vs Development:
- Development: configure own Azure OpenAI credentials via
SetSecretKey/SetEndpoint/SetDeployment - Production: use
SetManagedResourceAuthorization(Microsoft-managed, no secrets needed)
Phase 2: PromptDialog Page
Page Areas
| Area | Purpose | Contains |
|---|---|---|
PromptOptions | User settings/filters | Option/Enum fields only |
Prompt | User text input | Free-text field with InstructionalText |
Content | AI output display | Text field or part subpage with results |
PromptGuide (actions) | Example prompts | Actions that pre-fill the Prompt field |
SystemActions (actions) | Generate / OK / Cancel | systemaction(Generate), systemaction(OK), etc. |
PromptMode Options
| Mode | Behavior | Use when |
|---|---|---|
Prompt | Shows input first, user clicks Generate | User needs to provide context |
Generate | Auto-runs generation when page opens | Context is pre-filled from calling page |
Content | Shows content only, no generation | Displaying previously generated results |
Pattern: Complete PromptDialog Page
alnamespace Contoso.CopilotFeatures; using System.AI; page 50110 "Contoso Sales Forecast Copilot" { PageType = PromptDialog; Extensible = false; IsPreview = true; Caption = 'Sales Forecast with Copilot'; PromptMode = Prompt; layout { area(PromptOptions) { field(ForecastPeriod; SelectedPeriod) { ApplicationArea = All; Caption = 'Forecast Period'; ToolTip = 'Select the forecast time horizon.'; } } area(Prompt) { field(UserInput; UserPromptText) { ShowCaption = false; MultiLine = true; ApplicationArea = All; InstructionalText = 'Describe what you want to forecast (e.g., "top 10 items for next quarter")'; trigger OnValidate() begin CurrPage.Update(); end; } } area(Content) { // Option A: simple text response field(AIResponse; AIResponseText) { ApplicationArea = All; Caption = 'Copilot Suggestion'; MultiLine = true; Editable = false; } // Option B: structured results via subpage // part(Proposals; "Contoso Forecast Proposal Sub") // { // ApplicationArea = All; // } } } actions { area(PromptGuide) { action(ExampleTopItems) { ApplicationArea = All; Caption = 'Top selling items next quarter'; ToolTip = 'Predict the best-selling items for the next quarter.'; trigger OnAction() begin UserPromptText := 'What will be the top 10 selling items next quarter based on historical sales?'; CurrPage.Update(false); end; } action(ExampleSlowMovers) { ApplicationArea = All; Caption = 'Slow-moving inventory'; ToolTip = 'Identify items with declining sales trends.'; trigger OnAction() begin UserPromptText := 'Which items show declining sales over the last 6 months?'; CurrPage.Update(false); end; } } area(SystemActions) { systemaction(Generate) { Caption = 'Generate'; ToolTip = 'Generate AI forecast suggestions.'; trigger OnAction() begin RunGeneration(); end; } systemaction(Regenerate) { Caption = 'Regenerate'; ToolTip = 'Generate different suggestions.'; trigger OnAction() begin RunGeneration(); end; } systemaction(OK) { Caption = 'Keep it'; ToolTip = 'Accept and apply the forecast.'; } systemaction(Cancel) { Caption = 'Discard'; ToolTip = 'Discard suggestions.'; } } } trigger OnQueryClosePage(CloseAction: Action): Boolean begin if CloseAction = CloseAction::OK then ApplySuggestions(); end; local procedure RunGeneration() var GenerationCU: Codeunit "Contoso Forecast Generation"; begin AIResponseText := ''; GenerationCU.SetUserPrompt(UserPromptText); GenerationCU.SetPeriod(SelectedPeriod); if GenerationCU.Run() then AIResponseText := GenerationCU.GetCompletionResult() else Error('Generation failed: %1', GetLastErrorText()); CurrPage.Update(false); end; local procedure ApplySuggestions() begin // Apply user-approved results to BC data end; /// Call from external page to set context before opening procedure SetItemFilter(ItemCategoryCode: Code[20]) begin ContextItemCategory := ItemCategoryCode; end; var UserPromptText: Text; AIResponseText: Text; SelectedPeriod: Option "Next Month","Next Quarter","Next Year"; ContextItemCategory: Code[20]; }
Pattern: Temporary Table for Structured Output
When AI returns a list of proposals (not just text), use a temporary table:
altable 50110 "Contoso Forecast Proposal" { TableType = Temporary; Caption = 'Forecast Proposal'; fields { field(1; "Entry No."; Integer) { AutoIncrement = true; } field(10; "Item No."; Code[20]) { Caption = 'Item No.'; } field(20; Description; Text[100]) { Caption = 'Description'; } field(30; "Forecast Qty"; Decimal) { Caption = 'Forecast Quantity'; } field(40; Explanation; Text[250]) { Caption = 'AI Explanation'; } field(50; "Confidence Score"; Decimal) { Caption = 'Confidence'; MinValue = 0; MaxValue = 1; } } keys { key(PK; "Entry No.") { Clustered = true; } } }
Display via a ListPart subpage linked by part() in the Content area.
Phase 3: AI Generation Codeunit
Builds the Azure OpenAI chat-completion codeunit and the structured-output handling (temporary tables).
When implementing the generation codeunit, load references/copilot-ai-generation.md.
Phase 4: Testing with AI Test Toolkit
Validates the Copilot experience with the AI Test Toolkit: dependency setup, the test codeunit pattern, and the test workflow.
When writing Copilot tests, load references/copilot-testing.md.
Workflow
Step 1: Design Copilot Experience
Define before coding:
- User problem — what task does this Copilot help with?
- PromptMode — Prompt (user types) vs Generate (auto-run)?
- Input — free text, options, context from calling page?
- Output — simple text or structured proposals (temp table + subpage)?
- AI model — Temperature (deterministic vs creative), MaxTokens
Step 2: Implement (Phase 1 → Phase 3)
- Register capability (Phase 1: enum + install codeunit)
- Create PromptDialog page (Phase 2: areas, system actions, prompt guide)
- Create generation codeunit (Phase 3: Azure OpenAI + JSON parsing)
- Build:
al_build
Step 3: Test (Phase 4)
- Add AI Test Toolkit dependency to Test app
- Create test codeunit with happy path, edge cases, consistency, performance
- Create AI Test Suite dataset in BC for systematic prompt evaluation
- Run tests and iterate on prompts
Step 4: Responsible AI Review
Before shipping:
- User transparency — users know they're interacting with AI
- Content filtering — no raw AI output without validation
- Data privacy — no sensitive data in prompts without sanitization
- Feedback — users can accept/reject AI suggestions (OK/Cancel)
- Error handling — graceful handling of all Azure OpenAI error codes
References
- Build Copilot Capability in AL
- Build Copilot User Experience
- PromptDialog Page Type
- Azure OpenAI Module in AL
- AI Test Toolkit
- BCTech Samples — Copilot
- Responsible AI — Microsoft
Constraints
- Do NOT expose raw AI responses without validation — always parse and verify structure
- Do NOT include sensitive customer data in prompts without sanitization
- Do NOT deploy Copilot features without Responsible AI compliance review
- Do NOT skip AI Test Toolkit testing — every Copilot feature MUST have test coverage
- Do NOT use deprecated
SetAuthorizationin production — useSetManagedResourceAuthorization - Do NOT hardcode Azure OpenAI credentials — always use
IsolatedStorage - Permission set generation →
skill-permissions.md - Debugging AI integration issues →
skill-debug.md - Test strategy design →
skill-testing.md


