BC Agent Toolkit Skill
Build complete Business Central agents using the AI Development Toolkit and Agent SDK.
Two Development Paths
| Path | Use When | What You Create |
|---|---|---|
| Designer (No-Code) | Prototyping, testing | Agent via wizard with instructions/profile/permissions |
| SDK (Pro-Code) | Production, extensions | Coded agent via AL interfaces, shipped in .app |
Project Structure (from Agent Template)
app/
├── .resources/Instructions/InstructionsV1.txt # Agent instructions (resource file)
├── Example/
│ ├── {Agent}CustomerCardExt.PageExt.al # Page extension (task trigger example)
│ └── {Agent}PublicAPI.Codeunit.al # Public API + Implementation
├── Integration/
│ ├── {Agent}CopilotCapability.EnumExt.al # Copilot Capability enum
│ ├── {Agent}Install.Codeunit.al # Install handler
│ └── {Agent}Upgrade.Codeunit.al # Upgrade handler
└── Setup/
├── {Agent}Setup.Codeunit.al # Centralized setup logic (instructions, profile, permissions)
├── {Agent}Setup.Page.al # ConfigurationDialog page
├── {Agent}Setup.Table.al # Setup table (PK = User Security ID)
├── KPI/
│ ├── {Agent}KPI.Page.al # Summary/hover card (CardPart)
│ └── {Agent}KPI.Table.al # KPI tracking table
├── Metadata/
│ ├── {Agent}Factory.Codeunit.al # IAgentFactory implementation
│ ├── {Agent}Metadata.Codeunit.al # IAgentMetadata implementation
│ └── {Agent}MetadataProvider.EnumExt.al # Agent Metadata Provider enum
├── Permissions/
│ └── {Agent}.permissionset.al # PermissionSet (includes D365 BASIC)
├── Profile/
│ ├── {Agent}Profile.Profile.al # Agent profile
│ ├── {Agent}RoleCenter.Page.al # Agent role center
│ └── {Agent}*.PageCustomization.al # Page customizations
└── TaskExecution/
└── {Agent}TaskExecution.Codeunit.al # IAgentTaskExecution implementation
Agent SDK Architecture
Registration (3 objects)
// 1. Extend Copilot Capability — feature switch
enumextension 52100 "My Agent Copilot Capability" extends "Copilot Capability"
{
value(52100; "My Agent Capability")
{
Caption = 'My Agent';
}
}
// 2. Extend Agent Metadata Provider — links to 3 interface implementations
enumextension 52101 "My Agent Metadata Provider" extends "Agent Metadata Provider"
{
value(52101; "My Agent")
{
Caption = 'My Agent';
Implementation = IAgentFactory = MyAgentFactory,
IAgentMetadata = MyAgentMetadata,
IAgentTaskExecution = MyAgentTaskExecution;
}
}
// 3. Install codeunit — register + unregister on install
codeunit 52101 "My Agent Install"
{
Subtype = Install;
Access = Internal;
InherentEntitlements = X;
InherentPermissions = X;
trigger OnInstallAppPerDatabase()
begin
RegisterCapability();
// Re-install instructions for existing agents
var MyAgentSetup: Record "My Agent Setup";
if MyAgentSetup.FindSet() then
repeat
InstallAgent(MyAgentSetup);
until MyAgentSetup.Next() = 0;
end;
local procedure RegisterCapability()
var
CopilotCapability: Codeunit "Copilot Capability";
LearnMoreUrlTxt: Label 'https://docs.example.com', Locked = true;
begin
if CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"My Agent Capability") then
CopilotCapability.UnregisterCapability(Enum::"Copilot Capability"::"My Agent Capability");
CopilotCapability.RegisterCapability(
Enum::"Copilot Capability"::"My Agent Capability",
Enum::"Copilot Availability"::Preview,
"Copilot Billing Type"::"Microsoft Billed",
LearnMoreUrlTxt);
end;
}
3 Core Interfaces — Correct Signatures
IAgentFactory — Creation & defaults:
codeunit 52100 MyAgentFactory implements IAgentFactory
{
Access = Internal;
procedure GetDefaultInitials(): Text[4]
begin
exit(MyAgentSetupCU.GetInitials());
end;
procedure GetFirstTimeSetupPageId(): Integer
begin
exit(MyAgentSetupCU.GetSetupPageId());
end;
procedure ShowCanCreateAgent(): Boolean
begin
exit(true); // or single-instance logic
end;
procedure GetCopilotCapability(): Enum "Copilot Capability"
begin
exit("Copilot Capability"::"My Agent Capability");
end;
procedure GetDefaultProfile(var TempAllProfile: Record "All Profile" temporary)
begin
MyAgentSetupCU.GetDefaultProfile(TempAllProfile);
end;
procedure GetDefaultAccessControls(var TempAccessControlTemplate: Record "Access Control Buffer" temporary)
begin
MyAgentSetupCU.GetDefaultAccessControls(TempAccessControlTemplate);
end;
var
MyAgentSetupCU: Codeunit "My Agent Setup";
}
IAgentMetadata — Runtime UI:
codeunit 52102 MyAgentMetadata implements IAgentMetadata
{
Access = Internal;
procedure GetInitials(AgentUserId: Guid): Text[4]
begin
exit(MyAgentSetupCU.GetInitials());
end;
procedure GetSetupPageId(AgentUserId: Guid): Integer
begin
exit(MyAgentSetupCU.GetSetupPageId());
end;
procedure GetSummaryPageId(AgentUserId: Guid): Integer
begin
exit(MyAgentSetupCU.GetSummaryPageId());
end;
procedure GetAgentTaskMessagePageId(AgentUserId: Guid; MessageId: Guid): Integer
begin
exit(Page::"Agent Task Message Card"); // default or custom
end;
procedure GetAgentAnnotations(AgentUserId: Guid; var Annotations: Record "Agent Annotation")
begin
Clear(Annotations);
// Validate preconditions: licensing, configuration completeness
end;
var
MyAgentSetupCU: Codeunit "My Agent Setup";
}
IAgentTaskExecution — Task processing:
codeunit 52104 MyAgentTaskExecution implements IAgentTaskExecution
{
Access = Internal;
procedure AnalyzeAgentTaskMessage(
AgentTaskMessage: Record "Agent Task Message";
var Annotations: Record "Agent Annotation")
begin
if AgentTaskMessage.Type = AgentTaskMessage.Type::Output then
PostProcessOutputMessage(AgentTaskMessage, Annotations)
else
ValidateInputMessage(AgentTaskMessage, Annotations);
end;
procedure GetAgentTaskUserInterventionSuggestions(
AgentTaskUserInterventionRequestDetails: Record "Agent User Int Request Details";
var Suggestions: Record "Agent Task User Int Suggestion")
begin
if AgentTaskUserInterventionRequestDetails.Type =
AgentTaskUserInterventionRequestDetails.Type::Assistance then begin
Suggestions.Summary := 'User-friendly summary';
Suggestions.Description := 'System-facing condition (Locked)';
Suggestions.Instructions := 'Steps for agent after user acts (Locked)';
Suggestions.Insert();
end;
end;
procedure GetAgentTaskPageContext(
AgentTaskPageContextRequest: Record "Agent Task Page Context Req.";
var AgentTaskPageContext: Record "Agent Task Page Context")
begin
// Populate page-specific context for the agent
end;
local procedure ValidateInputMessage(
AgentTaskMessage: Record "Agent Task Message";
var Annotations: Record "Agent Annotation")
var
AgentMessage: Codeunit "Agent Message";
MessageText: Text;
begin
MessageText := AgentMessage.GetText(AgentTaskMessage);
// Add Error annotation to stop task
// Add Warning annotation to trigger user intervention
end;
local procedure PostProcessOutputMessage(
AgentTaskMessage: Record "Agent Task Message";
var Annotations: Record "Agent Annotation")
var
AgentMessage: Codeunit "Agent Message";
OldText: Text;
begin
OldText := AgentMessage.GetText(AgentTaskMessage);
AgentMessage.UpdateText(AgentTaskMessage, OldText + '\n---\nAgent Signature');
end;
}
Setup Codeunit (Centralized Logic)
The template introduces a Setup Codeunit that centralizes all agent configuration:
codeunit 52103 "My Agent Setup"
{
Access = Internal;
procedure GetInitials(): Text[4]
begin
exit('AGT');
end;
procedure GetSetupPageId(): Integer
begin
exit(Page::"My Agent Setup");
end;
procedure GetSummaryPageId(): Integer
begin
exit(Page::"My Agent KPI");
end;
[NonDebuggable]
procedure GetInstructions(): SecretText
begin
exit(NavApp.GetResourceAsText('Instructions/InstructionsV1.txt'));
end;
procedure GetDefaultProfile(var TempAllProfile: Record "All Profile" temporary)
var
CurrentModuleInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(CurrentModuleInfo);
Agent.PopulateDefaultProfile('MY AGENT PROFILE', CurrentModuleInfo.Id, TempAllProfile);
end;
procedure GetDefaultAccessControls(var TempAccessControlBuffer: Record "Access Control Buffer" temporary)
var
CurrentModuleInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(CurrentModuleInfo);
TempAccessControlBuffer."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(TempAccessControlBuffer."Company Name"));
TempAccessControlBuffer.Scope := TempAccessControlBuffer.Scope::System;
TempAccessControlBuffer."App ID" := CurrentModuleInfo.Id;
TempAccessControlBuffer."Role ID" := 'MY AGENT';
TempAccessControlBuffer.Insert();
end;
procedure InitializeSetupRecord(var TempSetup: Record "My Agent Setup" temporary; var AgentSetupBuffer: Record "Agent Setup Buffer")
var
AgentSetup: Codeunit "Agent Setup";
begin
// Load existing or set defaults
if AgentSetupBuffer.IsEmpty() then
AgentSetup.GetSetupRecord(
AgentSetupBuffer,
TempSetup."User Security ID",
Enum::"Agent Metadata Provider"::"My Agent",
'My Agent - ' + CompanyName(),
'My Agent',
'Description of what the agent does.');
end;
procedure SaveSetupRecord(var TempSetup: Record "My Agent Setup" temporary; var AgentSetupBuffer: Record "Agent Setup Buffer")
var
AgentSetup: Codeunit "Agent Setup";
IsNewAgent: Boolean;
begin
IsNewAgent := IsNullGuid(AgentSetupBuffer."User Security ID");
if AgentSetup.GetChangesMade(AgentSetupBuffer) then begin
TempSetup."User Security ID" := AgentSetup.SaveChanges(AgentSetupBuffer);
if IsNewAgent then
Agent.SetInstructions(TempSetup."User Security ID", GetInstructions());
end;
end;
var
Agent: Codeunit Agent;
}
ConfigurationDialog Page
page 52100 "My Agent Setup"
{
PageType = ConfigurationDialog;
Extensible = false;
SourceTable = "My Agent Setup";
SourceTableTemporary = true;
InherentEntitlements = X;
InherentPermissions = X;
layout
{
area(Content)
{
part(AgentSetupPart; "Agent Setup Part") // MUST be first
{
ApplicationArea = All;
UpdatePropagation = Both;
}
// Custom groups below...
}
}
actions
{
area(SystemActions)
{
systemaction(OK) { Caption = 'Update'; Enabled = IsUpdated; }
systemaction(Cancel) { Caption = 'Cancel'; }
}
}
trigger OnOpenPage()
begin
if not AzureOpenAI.IsEnabled(Enum::"Copilot Capability"::"My Agent Capability") then
Error(CapabilityNotEnabledErr);
InitializePage();
end;
trigger OnQueryClosePage(CloseAction: Action): Boolean
begin
if CloseAction = CloseAction::Cancel then exit(true);
CurrPage.AgentSetupPart.Page.GetAgentSetupBuffer(AgentSetupBuffer);
MyAgentSetupCU.SaveSetupRecord(Rec, AgentSetupBuffer);
MyAgentSetupCU.SaveCustomProperties(Rec);
exit(true);
end;
}
Public API Pattern
codeunit 52106 "My Agent Public API"
{
Access = Public;
procedure AssignTask(AgentUserSecurityID: Guid; TaskTitle: Text[150]; From: Text[250]; Message: Text): Record "Agent Task"
begin
exit(Impl.AssignTask(AgentUserSecurityID, TaskTitle, From, Message));
end;
// Overloads for ExternalId, Attachments, etc.
var
Impl: Codeunit "My Agent Public API Impl.";
}
Task Creation (via Agent Task Builder)
// Inside the Impl codeunit:
local procedure AssignTaskInternal(...): Record "Agent Task"
var
AgentTaskBuilder: Codeunit "Agent Task Builder";
begin
AgentTaskBuilder := AgentTaskBuilder
.Initialize(AgentUserSecurityID, TaskTitle)
.AddTaskMessage(From, Message);
if ExternalId <> '' then
AgentTaskBuilder.SetExternalId(ExternalId);
exit(AgentTaskBuilder.Create());
end;
Agent Session Detection
// Detect any agent session
var AgentSession: Codeunit "Agent Session";
AgentMetadataProvider: Enum "Agent Metadata Provider";
begin
if not AgentSession.IsAgentSession(AgentMetadataProvider) then exit;
end;
// Performance: bind subscribers only for agent task duration
[EventSubscriber(ObjectType::Codeunit, Codeunit::"System Initialization", OnAfterInitialization, '', false, false)]
local procedure OnInit()
begin
if not AgentSession.IsAgentSession(Provider::"My Agent") then exit;
GlobalEvents.SetTaskID(AgentSession.GetCurrentSessionAgentTaskId());
BindSubscription(GlobalEvents);
end;
Task Integration Patterns
See references/workflow-task.md for all 8 patterns.
Quick Start
VS Code: Ctrl+Shift+P → AL: New Project → choose Agent template.
Or run scaffold: python scripts/scaffold_agent.py "Agent Name" ./src
Workflows
| Need | Reference |
|---|---|
| Full coded agent | references/workflow-create.md |
| Task integration | references/workflow-task.md |
| Agent instructions | references/workflow-instructions.md |
| Test generation | references/workflow-test.md |
Troubleshooting
| Symptom | Check |
|---|---|
| Agent doesn't appear | Copilot capability registered? Install codeunit ran? |
| Can't create instance | ShowCanCreateAgent() returns false? |
| Setup page error | SourceTableTemporary = true? AgentSetupPart first? Extensible = false? |
| Wrong defaults | Check Setup Codeunit: GetDefaultProfile / GetDefaultAccessControls |
| Input rejected | AnalyzeAgentTaskMessage → Error annotation on input? |
| No suggestions | GetAgentTaskUserInterventionSuggestions → empty? Type filter? |
| Events not firing | BindSubscription in agent session? SingleInstance? |
| Agent loses context | MEMORIZE in instructions? Example format provided? |
| Capability not found | AzureOpenAI.IsEnabled() → check Copilot & Agent Capabilities |