Events and subscriptions represent the cornerstone of modern Business Central development architecture. This publisher-subscriber pattern enables developers to extend system functionality without modifying base application code, ensuring upgrade compatibility and maintainable solutions.
The AL event system operates on a decoupled architecture where event publishers announce significant occurrences, and event subscribers respond to these announcements. This approach supports multiple extensions working together without direct dependencies, creating a flexible and scalable development environment.
Integration events provide the most flexible extension points for custom functionality:
// Publishing an Integration Event
codeunit 50100 "Customer Management"
{
[IntegrationEvent(false, false)]
procedure OnBeforeCreateCustomer(var Customer: Record Customer; var IsHandled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
procedure OnAfterCreateCustomer(Customer: Record Customer)
begin
end;
procedure CreateCustomer(CustomerData: Record Customer)
var
IsHandled: Boolean;
begin
OnBeforeCreateCustomer(CustomerData, IsHandled);
if not IsHandled then begin
CustomerData.Insert(true);
OnAfterCreateCustomer(CustomerData);
end;
end;
}
Integration event characteristics:
Business events represent specific business operations and their outcomes:
// Business Event for sales processing
codeunit 50101 "Sales Order Processing"
{
[BusinessEvent(false)]
procedure OnSalesOrderShipped(SalesOrderNo: Code[20]; ShippingDate: Date; TrackingNo: Text[50])
begin
end;
procedure ProcessShipment(SalesOrderNo: Code[20])
var
SalesHeader: Record "Sales Header";
TrackingNumber: Text[50];
begin
// Shipment processing logic
SalesHeader.Get(SalesHeader."Document Type"::Order, SalesOrderNo);
TrackingNumber := GenerateTrackingNumber();
// Raise business event
OnSalesOrderShipped(SalesOrderNo, Today, TrackingNumber);
end;
}
Business event applications:
Database events respond to table operations automatically:
// Database events on Customer table
tableextension 50100 "Customer Events" extends Customer
{
trigger OnAfterInsert()
begin
NotifyCustomerCreated();
end;
trigger OnAfterModify()
begin
if Rec."Credit Limit (LCY)" <> xRec."Credit Limit (LCY)" then
NotifyCreditLimitChanged();
end;
local procedure NotifyCustomerCreated()
var
CustomerNotification: Codeunit "Customer Notification";
begin
CustomerNotification.SendWelcomeEmail(Rec);
end;
local procedure NotifyCreditLimitChanged()
var
ApprovalManagement: Codeunit "Custom Approval Management";
begin
if Rec."Credit Limit (LCY)" > 10000 then
ApprovalManagement.RequestCreditLimitApproval(Rec);
end;
}
Implementing smart event handling with conditional logic:
// Sophisticated event subscriber with business logic
codeunit 50102 "Advanced Customer Handler"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Customer Management", 'OnBeforeCreateCustomer', '', false, false)]
local procedure ValidateCustomerData(var Customer: Record Customer; var IsHandled: Boolean)
var
DuplicateCustomer: Record Customer;
ValidationError: Text;
begin
// Duplicate check
DuplicateCustomer.SetRange("VAT Registration No.", Customer."VAT Registration No.");
if not DuplicateCustomer.IsEmpty then begin
ValidationError := 'Customer with VAT number %1 already exists';
Error(ValidationError, Customer."VAT Registration No.");
end;
// Credit check for high-value customers
if Customer."Credit Limit (LCY)" > 50000 then begin
if not PerformCreditCheck(Customer) then begin
IsHandled := true;
CreatePendingApprovalRecord(Customer);
end;
end;
end;
local procedure PerformCreditCheck(Customer: Record Customer): Boolean
var
CreditCheckService: Codeunit "Credit Check Service";
begin
exit(CreditCheckService.ValidateCustomerCredit(Customer));
end;
local procedure CreatePendingApprovalRecord(Customer: Record Customer)
var
PendingApproval: Record "Pending Customer Approval";
begin
PendingApproval.Init();
PendingApproval.TransferFields(Customer);
PendingApproval."Approval Status" := PendingApproval."Approval Status"::Pending;
PendingApproval.Insert();
end;
}
Creating complex workflows through event orchestration:
// Multi-step business process orchestration
codeunit 50103 "Order Processing Orchestrator"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', false, false)]
local procedure OnSalesOrderPosted(var SalesHeader: Record "Sales Header")
begin
case SalesHeader."Document Type" of
SalesHeader."Document Type"::Order:
ProcessPostedSalesOrder(SalesHeader);
end;
end;
local procedure ProcessPostedSalesOrder(SalesHeader: Record "Sales Header")
var
InventoryManagement: Codeunit "Inventory Management";
ShippingProcessor: Codeunit "Shipping Processor";
InvoiceProcessor: Codeunit "Invoice Processor";
begin
// Step 1: Update inventory allocations
InventoryManagement.UpdateAllocations(SalesHeader."No.");
// Step 2: Generate shipping documentation
ShippingProcessor.CreateShippingDocuments(SalesHeader."No.");
// Step 3: Process invoicing if immediate billing
if SalesHeader."Invoice Discount Calculation" = SalesHeader."Invoice Discount Calculation"::"% (Normal VAT)" then
InvoiceProcessor.GenerateInvoice(SalesHeader."No.");
end;
}
// Error handling best practices in event subscribers
codeunit 50104 "Resilient Event Handler"
{
[EventSubscriber(ObjectType::Table, Database::Customer, 'OnAfterModifyEvent', '', false, false)]
local procedure HandleCustomerModification(var Rec: Record Customer)
var
ErrorMessage: Text;
TelemetryDictionary: Dictionary of [Text, Text];
begin
if not TryUpdateExternalSystems(Rec) then begin
ErrorMessage := GetLastErrorText();
LogError('Customer modification sync failed', ErrorMessage, Rec."No.");
// Don't fail the main transaction for external system issues
ClearLastError();
// Queue for retry processing
QueueForRetry(Rec);
end;
end;
[TryFunction]
local procedure TryUpdateExternalSystems(Customer: Record Customer)
var
ExternalCRM: Codeunit "External CRM Integration";
WebService: Codeunit "Customer Web Service";
begin
ExternalCRM.UpdateCustomer(Customer);
WebService.NotifyCustomerChange(Customer."No.");
end;
local procedure LogError(Context: Text; ErrorText: Text; CustomerNo: Code[20])
var
ActivityLog: Record "Activity Log";
begin
ActivityLog.LogActivity(
Database::Customer,
ActivityLog.Status::Failed,
Context,
ErrorText,
CustomerNo);
end;
local procedure QueueForRetry(Customer: Record Customer)
var
RetryQueue: Record "Integration Retry Queue";
begin
RetryQueue.Init();
RetryQueue."Record ID" := Customer.RecordId;
RetryQueue."Retry Count" := 0;
RetryQueue."Next Retry Time" := CurrentDateTime + 300000; // 5 minutes
RetryQueue.Insert();
end;
}
Optimizing event subscriber performance:
// Performance-optimized event handling
codeunit 50105 "Optimized Event Handler"
{
var
IsInitialized: Boolean;
ConfigurationCache: Dictionary of [Text, Text];
[EventSubscriber(ObjectType::Table, Database::"Sales Line", 'OnAfterModifyEvent', '', false, false)]
local procedure OptimizedSalesLineHandler(var Rec: Record "Sales Line")
begin
// Early exit conditions to avoid unnecessary processing
if not ShouldProcessSalesLine(Rec) then
exit;
// Initialize cache only when needed
if not IsInitialized then
InitializeCache();
// Batch operations for multiple line changes
if not IsProcessingBatch() then
ProcessSalesLineChange(Rec)
else
QueueBatchOperation(Rec);
end;
local procedure ShouldProcessSalesLine(SalesLine: Record "Sales Line"): Boolean
begin
// Only process if meaningful fields changed
exit(
(SalesLine.Quantity <> xRec.Quantity) or
(SalesLine."Unit Price" <> xRec."Unit Price") or
(SalesLine."Line Discount %" <> xRec."Line Discount %")
);
end;
local procedure InitializeCache()
var
Setup: Record "Sales & Receivables Setup";
begin
Setup.Get();
ConfigurationCache.Add('AutoPostInvoice', Format(Setup."Auto Post Invoice"));
ConfigurationCache.Add('RequireApproval', Format(Setup."Require Approval"));
IsInitialized := true;
end;
}
// Well-documented event publisher with clear contracts
codeunit 50106 "Purchase Order Management"
{
/// <summary>
/// Raised before a purchase order is converted to an invoice.
/// Allows subscribers to validate data or prevent conversion.
/// </summary>
/// <param name="PurchaseHeader">The purchase order being converted</param>
/// <param name="IsHandled">Set to true to prevent default conversion</param>
/// <param name="SkipValidation">Set to true to skip standard validations</param>
[IntegrationEvent(false, false)]
procedure OnBeforeConvertPurchaseOrderToInvoice(var PurchaseHeader: Record "Purchase Header"; var IsHandled: Boolean; var SkipValidation: Boolean)
begin
end;
/// <summary>
/// Raised after successful purchase order conversion to invoice.
/// Use for notifications, logging, or triggering dependent processes.
/// </summary>
/// <param name="OriginalOrderNo">The original purchase order number</param>
/// <param name="NewInvoiceNo">The generated invoice number</param>
/// <param name="ConversionDetails">Additional conversion information</param>
[IntegrationEvent(false, false)]
procedure OnAfterConvertPurchaseOrderToInvoice(OriginalOrderNo: Code[20]; NewInvoiceNo: Code[20]; ConversionDetails: Dictionary of [Text, Text])
begin
end;
}
// Test codeunit for event functionality
codeunit 50150 "Event Handler Tests"
{
Subtype = Test;
var
EventWasRaised: Boolean;
EventParameters: List of [Text];
[Test]
procedure TestCustomerCreationEvent()
var
Customer: Record Customer;
CustomerMgt: Codeunit "Customer Management";
begin
// Setup
EventWasRaised := false;
EventParameters.Clear();
// Execute
Customer.Init();
Customer."No." := 'TEST001';
Customer.Name := 'Test Customer';
CustomerMgt.CreateCustomer(Customer);
// Verify
Assert.IsTrue(EventWasRaised, 'Customer creation event should have been raised');
Assert.AreEqual('TEST001', EventParameters.Get(1), 'Event should contain customer number');
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Customer Management", 'OnAfterCreateCustomer', '', false, false)]
local procedure CaptureCustomerCreationEvent(Customer: Record Customer)
begin
EventWasRaised := true;
EventParameters.Add(Customer."No.");
EventParameters.Add(Customer.Name);
end;
}
// Domain-specific event handlers following microservice patterns
codeunit 50107 "Inventory Domain Handler"
{
// Handle all inventory-related events in one domain handler
[EventSubscriber(ObjectType::Table, Database::Item, 'OnAfterModifyEvent', '', false, false)]
local procedure HandleItemChange(var Rec: Record Item)
begin
ProcessItemMasterDataChange(Rec);
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Jnl.-Post Line", 'OnAfterPostItemJnlLine', '', false, false)]
local procedure HandleInventoryTransaction(var ItemJournalLine: Record "Item Journal Line")
begin
ProcessInventoryMovement(ItemJournalLine);
end;
local procedure ProcessItemMasterDataChange(Item: Record Item)
var
ChangeDetector: Codeunit "Item Change Detector";
NotificationService: Codeunit "Inventory Notification Service";
begin
if ChangeDetector.IsPriceChange(Item, xRec) then
NotificationService.NotifyPriceChange(Item);
if ChangeDetector.IsAvailabilityChange(Item, xRec) then
NotificationService.NotifyAvailabilityChange(Item);
end;
}
// Configurable event subscription management
codeunit 50108 "Event Subscription Manager"
{
procedure EnableIntegrationFeature(FeatureName: Text)
var
IntegrationSetup: Record "Integration Feature Setup";
begin
IntegrationSetup.SetRange("Feature Name", FeatureName);
if IntegrationSetup.FindFirst() then begin
IntegrationSetup.Enabled := true;
IntegrationSetup.Modify();
end;
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Customer Management", 'OnAfterCreateCustomer', '', false, false)]
local procedure ConditionalCustomerHandler(Customer: Record Customer)
var
IntegrationSetup: Record "Integration Feature Setup";
begin
IntegrationSetup.SetRange("Feature Name", 'EXTERNAL_CRM_SYNC');
IntegrationSetup.SetRange(Enabled, true);
if IntegrationSetup.FindFirst() then
ProcessExternalCRMSync(Customer);
end;
local procedure ProcessExternalCRMSync(Customer: Record Customer)
var
ExternalSync: Codeunit "External CRM Sync";
begin
ExternalSync.SyncCustomer(Customer);
end;
}
Understanding and effectively implementing events and subscriptions in AL enables developers to create sophisticated, maintainable Business Central extensions that integrate seamlessly with the base application while remaining upgrade-compatible. This event-driven architecture supports complex business requirements while maintaining clean code separation and testability.
The AL event system provides the foundation for building enterprise-grade solutions that can evolve with changing business needs while preserving existing functionality and ensuring long-term maintainability.
Unlock the full potential of Business Central for finance teams with this step-by-step guide on automation, best practices, reporting, and integration strategies.
Kery Nguyen
2025-05-02
A comprehensive guide for finance professionals on how to implement and manage fixed asset processes using ERP systems. Covers setup, categorization, depreciation, disposals, and internal controls for compliance and operational efficiency.
Kery Nguyen
2025-05-02
This detailed guide explores how to effectively integrate Microsoft's Business Central with Dataverse and Power Apps, enhancing business operations through synergy and sophisticated data management.
Kery Nguyen
2025-05-02