Download our free white paper on Copilot in Microsoft Business Central! Download Now

Mastering Events and Subscriptions in AL for Microsoft Dynamics 365 Business Central

Kery Nguyen
By Kery Nguyen

2025-05-02

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.

Event Types and Their Applications

Integration Events

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:

  • Provide extension points without affecting base functionality
  • Support conditional execution through "IsHandled" patterns
  • Allow multiple subscribers without interference
  • Maintain upgrade compatibility across Business Central versions

Business Events

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:

  • Workflow trigger points for business process automation
  • Integration checkpoints for external system notifications
  • Audit trail generation for compliance requirements
  • Analytics data collection for business intelligence

Database Events

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;
}

Advanced Event Subscription Patterns

Conditional Event Handling

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;
}

Event Chaining and Orchestration

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 and Performance Optimization

Robust Error Management in Event Subscribers

// 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;
}

Performance Considerations

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;
}

Event Documentation and Maintenance

Self-Documenting Event Patterns

// 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;
}

Testing Event-Driven Functionality

Comprehensive Event Testing Strategies

// 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;
}

Event-Driven Architecture Patterns

Microservice-Style Event Handling

// 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;
}

Event Lifecycle Management

Dynamic Event Subscription Control

// 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.

AL ProgrammingMicrosoft Dynamics 365Business CentralEventsSubscriptionsSoftware Development
Choosing the right ERP consulting partner can make all the difference. At BusinessCentralNav, we combine deep industry insight with hands-on Microsoft Business Central expertise to help you simplify operations, improve visibility, and drive growth. Our approach is rooted in collaboration, transparency, and a genuine commitment to delivering real business value—every step of the way.

Let`'s talk

Explore Business Central Posts

image

Optimizing Financial Management with Business Central

Unlock the full potential of Business Central for finance teams with this step-by-step guide on automation, best practices, reporting, and integration strategies.

By

Kery Nguyen

Date

2025-05-02

image

Fixed Asset Management: Setup, Depreciation & Audit Tips

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.

By

Kery Nguyen

Date

2025-05-02

image

Integrating Business Central with Dataverse and Power Apps

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.

By

Kery Nguyen

Date

2025-05-02