In modern ERP implementations, automated communication is essential. Whether sending payment reminders, order confirmations, or shipment tracking notifications, manually writing and dispatching emails slows down business operations.
In Microsoft Dynamics 365 Business Central, Microsoft replaced legacy SMTP utilities with the modern System Application Email Module, offering built-in scenario mapping, logging, attachment handling, and background processing. Combined with native Azure OpenAI (Copilot) capabilities, Business Central can dynamically draft personalized, context-aware email content based on live ERP data.
This guide provides a production-ready, step-by-step walk-through for building an end-to-end AI email dispatching solution in Business Central using AL.
- Prerequisites
Before deploying this solution, ensure your development environment and Business Central tenant meet the following requirements:
Target Application Version: Business Central v26.0 or higher (compatible with AL Target version 16.0+).
Azure OpenAI Access: An active Azure subscription with an Azure OpenAI Service resource deployed (e.g., gpt-4o or gpt-4 model deployment) along with its Endpoint URL and API Key.
System Permissions: Super user or Administrator access in Business Central to configure Email Accounts, Scenarios, and Copilot Governance capabilities.
- Key Development Best Practices
1. Secure Credential Storage: Never hardcode Azure OpenAI API keys or credentials directly inside AL code files. Store them securely inside IsolatedStorage, setup tables with restricted access, or retrieve them dynamically via Azure Key Vault using Service-to-Service (S2S) authentication.
2. LLM Output Sanitization: AI models frequently return text wrapped in Markdown formatting (such as html tags). Always sanitize the AI response before assigning it to an email body to prevent raw Markdown tags from displaying in the recipient's inbox.
3. Graceful Fallbacks & Error Handling: Network calls to AI services can fail or time out. Ensure your code handles API failures gracefully without rolling back critical transactional business data.
4. MIME Type Accuracy: When attaching files to the standard Email Message codeunit, ensure correct MIME content types (e.g., application/pdf, image/png) are supplied to prevent corrupt attachments in email clients.
- Business Scenario: Automated AI Dunning Notifications
The Problem
Accounts Receivable teams spend significant time drafting customized follow-up emails for past-due balances. Generic bulk template emails are often ignored, whereas personalized reminders yield higher response rates but require significant manual labor.
The Solution
When a credit controller opens a Customer Card with an overdue balance, clicking "Send AI Overdue Reminder" triggers Business Central to:
1. Extract live balance details, customer name, and currency from the ledger.
2. Send this context to Azure OpenAI to draft a polite, professional, and personalized HTML payment notice.
3. Pass the AI-generated HTML body into Business Central's native System.Email module.
4. Dispatch the email directly to the customer's email address and log the transaction in the Email Outbox.
- Step-by-Step Implementation Guide
Follow these sequential steps to implement the complete architecture in your AL project, Sample code is listed below for reference, reach out me for more details.
Step 1: Create the Standard Email Management Codeunit
This codeunit encapsulates Business Central's native Email Message and Email codeunits to build HTML messages and handle file attachments.
/// <summary>
/// Codeunit for dispatching system emails in Business Central.
/// Implements standard system email architecture with error handling and attachment capabilities.
/// </summary>
codeunit 50100 "BKS Email Management"
{
Access = Public;
/// <summary>
/// Creates and sends an HTML formatted email message.
/// </summary>
/// <param name="ToRecipient">Primary email address recipient.</param>
/// <param name="Subject">Email subject line.</param>
/// <param name="BodyText">HTML formatted email body string.</param>
/// <param name="AttachmentInStream">InStream containing document attachment content.</param>
/// <param name="AttachmentFileName">Filename including extension for the attachment.</param>
/// <returns>True if the email was successfully sent or queued; otherwise false.</returns>
procedure SendNotificationEmail(
ToRecipient: Text[250];
Subject: Text[250];
BodyText: Text;
AttachmentInStream: InStream;
AttachmentFileName: Text[250]
): Boolean
var
EmailMessage: Codeunit "Email Message";
Email: Codeunit "Email";
Recipients: List of [Text];
IsSent: Boolean;
begin
if ToRecipient = '' then
Error('Recipient email address cannot be empty.');
// Add primary recipient to recipient collection
Recipients.Add(ToRecipient);
// Construct base message (Recipients, Subject, Body, HtmlFormatted)
EmailMessage.Create(Recipients, Subject, BodyText, true);
// Attach document if InStream contains valid data
if (AttachmentFileName <> '') and (AttachmentInStream.Length > 0) then
EmailMessage.AddAttachment(
AttachmentFileName,
GetContentType(AttachmentFileName),
AttachmentInStream
);
// Send via standard default scenario or queue background process
IsSent := Email.Send(EmailMessage, Enum::"Email Scenario"::Default);
exit(IsSent);
end;
/// <summary>
/// Helper method to derive standard MIME content types from file extensions.
/// </summary>
local procedure GetContentType(FileName: Text): Text
begin
case true of
FileName.EndsWith('.pdf'):
exit('application/pdf');
FileName.EndsWith('.docx'):
exit('application/vnd.openxmlformats-officedocument.wordprocessingml.document');
FileName.EndsWith('.xlsx'):
exit('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
FileName.EndsWith('.png'):
exit('image/png');
FileName.EndsWith('.jpg'), FileName.EndsWith('.jpeg'):
exit('image/jpeg');
else
exit('application/octet-stream');
end;
end;
}
Step 2: Register the Copilot Capability
To register custom AI features within Business Central's native Copilot Governance framework, extend the Copilot Capability enum and register the feature on app installation.
Enum Extension: BKSCopilotCapabilitySetup.EnumExt.al
enumextension 50100 "BKS Copilot Capability Ext" extends "Copilot Capability"
{
value(50100; "Customer Collection Email")
{
Caption = 'Customer Collection Email AI Generator';
}
}
Install Codeunit: BKSRegisterCopilotCapability.Codeunit.al
/// <summary>
/// Installs and registers the custom Copilot capability in Business Central AI Governance.
/// </summary>
codeunit 50101 "BKS Register Copilot Cap."
{
Subtype = Install;
trigger OnInstallAppPerCompany()
begin
RegisterAIFeature();
end;
local procedure RegisterAIFeature()
var
CopilotCapability: Codeunit "Copilot Capability";
begin
// Register capability within standard Copilot Governance
if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"Customer Collection Email") then
CopilotCapability.RegisterCapability(
Enum::"Copilot Capability"::"Customer Collection Email",
Enum::"Copilot Availability"::Preview,
'[https://yourdomain.com/copilot-privacy-policy](https://yourdomain.com/copilot-privacy-policy)'
);
end;
}
Step 3: Implement the AI Generation Engine
This codeunit builds prompt payloads, securely triggers the Azure OpenAI system codeunit, and returns clean HTML content for the email body.
/// <summary>
/// Connects to Azure OpenAI Service to draft dynamic HTML email content based on ERP records.
/// </summary>
codeunit 50102 "BKS AI Email Generator"
{
Access = Public;
/// <summary>
/// Generates an HTML-formatted collection email body using Azure OpenAI integration.
/// </summary>
/// <param name="CustomerName">Name of the target customer account.</param>
/// <param name="OverdueBalance">Total amount past payment due date.</param>
/// <param name="CurrencyCode">Code of transaction currency (e.g., USD, EUR).</param>
/// <returns>Clean HTML formatted string returned by AI processing.</returns>
procedure GenerateCollectionEmailBody(
CustomerName: Text[100];
OverdueBalance: Decimal;
CurrencyCode: Code[10]
): Text
var
AzureOpenAI: Codeunit "Azure OpenAI";
CopilotCapability: Codeunit "Copilot Capability";
AIOperationResponse: Codeunit "AOAI Operation Response";
ChatMessages: Codeunit "AOAI Chat Messages";
Endpoint: Text;
DeploymentName: Text;
ApiKey: Text;
SystemPrompt: Text;
UserPrompt: Text;
GeneratedContent: Text;
begin
// Validate capability is registered and enabled
if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"Customer Collection Email") then
Error('The AI Collection Email capability is not registered.');
// Load credentials from secure storage
GetAzureOpenAICredentials(Endpoint, DeploymentName, ApiKey);
// Initialize Authorization and deployment configurations
AzureOpenAI.Initialize(Endpoint, DeploymentName, ApiKey);
// Define System Instructions (Metaprompt)
SystemPrompt := 'You are an automated professional finance assistant for Business Central. ' +
'Generate a firm, polite, and professional payment reminder email in clean HTML format. ' +
'Do not include markdown wrappers (such as ```html). Return valid HTML body tags only.';
ChatMessages.AddSystemMessage(SystemPrompt);
// Define User Contextual Prompt with ERP Data
UserPrompt := StrSubstNo(
'Draft a payment collection notice for Customer: %1. Total Overdue Amount: %2 %3.',
CustomerName,
Format(OverdueBalance, 0, '<Precision,2:2><Standard Format,0>'),
CurrencyCode
);
ChatMessages.AddUserMessage(UserPrompt);
// Execute API request to Azure OpenAI
AzureOpenAI.GenerateChatCompletion(ChatMessages, AIOperationResponse);
if AIOperationResponse.IsSuccess() then begin
GeneratedContent := ChatMessages.GetLastMessage();
exit(CleanGeneratedHTML(GeneratedContent));
end else
Error('AI Processing failed: %1', AIOperationResponse.GetError());
end;
local procedure CleanGeneratedHTML(RawText: Text): Text
begin
// Remove markdown backticks if returned by the language model
RawText := RawText.Replace('```html', '');
RawText := RawText.Replace('```', '');
exit(RawText.Trim());
end;
local procedure GetAzureOpenAICredentials(var Endpoint: Text; var DeploymentName: Text; var ApiKey: Text)
begin
// Replace with secure storage retrieval (e.g., IsolatedStorage)
Endpoint := 'https://your-resource.openai.azure.com/';
DeploymentName := 'gpt-4o';
ApiKey := 'YOUR_SECURE_AZURE_OPENAI_KEY';
end;
}
Step 4: Expose the Action on the Customer Card Page
Finally, add an action button to the Customer Card page to allow users to trigger the AI email workflow directly from the user interface.
/// <summary>
/// Page extension adding AI email generation actions to the Customer Card.
/// </summary>
pageextension 50100 "BKS Customer Card Ext" extends "Customer Card"
{
actions
{
addlast(Processing)
{
action("BKS_SendAIPaymentReminder")
{
ApplicationArea = All;
Caption = 'Send AI Overdue Reminder';
Image = SendEmail;
ToolTip = 'Generates an AI-tailored payment reminder email and dispatches it through the system email framework.';
trigger OnAction()
var
AIEmailGenerator: Codeunit "BKS AI Email Generator";
EmailManagement: Codeunit "BKS Email Management";
DummyInStream: InStream;
AIBodyHtml: Text;
SubjectText: Text;
IsSuccess: Boolean;
begin
Rec.TestField("E-Mail");
Rec.CalcFields("Balance Due (LCY)");
if Rec."Balance Due (LCY)" <= 0 then
Error('Customer %1 has no outstanding overdue balance.', Rec.Name);
// Step 1: Draft Email Content using AI
AIBodyHtml := AIEmailGenerator.GenerateCollectionEmailBody(
Rec.Name,
Rec."Balance Due (LCY)",
Rec."Currency Code"
);
// Step 2: Build Subject Line
SubjectText := StrSubstNo('Payment Reminder: Outstanding Account Status for %1', Rec.Name);
// Step 3: Dispatch Email
IsSuccess := EmailManagement.SendNotificationEmail(
Rec."E-Mail",
SubjectText,
AIBodyHtml,
DummyInStream,
''
);
if IsSuccess then
Message('Payment reminder email successfully sent to %1.', Rec."E-Mail")
else
Message('Email queued or failed to send. Check the Email Outbox.');
end;
}
}
}
}
- Testing & Sample Output
### Testing Procedure
1. Deploy the AL Extension package to your Business Central Sandbox environment.
2. Ensure an active Email Account is configured in Business Central under Email Accounts.
3. Open a Customer Card record (e.g., Trey Research) that has an overdue balance and a valid target email address assigned.
4. Click Actions > Processing > Send AI Overdue Reminder.
### Sample Rendered Email Output
Below is an exact visual representation of how the generated email arrives in the recipient's email client inbox:
──────────────────────────────────────────────────────────
From: Finance Department <billing@yourcompany.com>
To: Accounts Payable <ap@treyresearch.com>
Subject: Payment Reminder: Outstanding Account Status for Trey Research
Date: July 24, 2026, 8:30 AM
──────────────────────────────────────────────────────────
Statement of Account: Overdue Balance Reminder
Dear Trey Research Team,
We hope this email finds you well. Our records indicate that your account
currently has an outstanding balance that is past its agreed payment terms.
┌────────────────────────┬────────────────────────┐
│ Account Name │ Overdue Balance │
├────────────────────────┼────────────────────────┤
│ Trey Research │ 15,420.50 USD │
└────────────────────────┴────────────────────────┘
Please review this balance and arrange for remittance at your earliest convenience.
If payment has already been processed, kindly disregard this notice or reply with
the payment reference details so we can update your ledger.
Thank you for your prompt attention to this matter and for your continued partnership.
Best regards,
Accounts Receivable Department
Automated Financial Systems
──────────────────────────────────────────────────────────
### Summary
By combining the System Application Email Module with Azure OpenAI Services, Business Central developers can build intelligent, data-driven automation that delivers personalized communication while maintaining enterprise control, governance, and audit logging.
No comments:
Post a Comment