8.2 Events, Publishers, Subscribers & Event Isolation
Key Takeaways
- Event-driven architecture in AL decouples custom extension logic from the core Base Application, ensuring upgrade resilience without source code modification.
- IntegrationEvent is the primary extension point for developers, whereas BusinessEvent represents an immutable public contract with long-term signature stability.
- Event subscribers are declared using the [EventSubscriber] attribute, specifying the publisher object type, object ID, event name, element name, and licensing/permission flags.
- Event isolation (Isolated = true) ensures that failing subscribers execute in isolated transaction boundaries, preventing errors from rolling back peer subscribers.
- The IsHandled design pattern allows subscribers to intercept standard business routines and conditionally suppress default base application calculations or posting routines.
8.2 Events, Publishers, Subscribers & Event Isolation
The event-driven architecture is the cornerstone of modern Business Central extensibility. Prior to the AL extension model, customizations involved modifying base C/AL source code directly, creating complex merge conflicts during cumulative updates. In AL, developers cannot alter standard source files; instead, the platform exposes thousands of publisher events throughout core tables, pages, reports, and codeunits. Developers write subscriber procedures that listen for these events, injecting custom logic seamlessly without altering base code.
1. Event Publisher Types & Attributes
An event publisher is a procedure declaration decorated with an event attribute. Publisher procedures contain no AL body code (no begin...end block or variable definitions) and are invoked directly from operational code using their procedure name.
codeunit 50110 "Custom Order Processor"
{
// 1. Integration Event Publisher
[IntegrationEvent(true, false)]
procedure OnBeforeProcessCustomOrder(var SalesHeader: Record "Sales Header"; var IsHandled: Boolean)
begin
end;
// 2. Business Event Publisher (Formal Contract)
[BusinessEvent(false)]
procedure OnCustomerCreditLimitExceeded(CustomerNo: Code[20]; OverdueBalance: Decimal)
begin
end;
// 3. Internal Event Publisher (Module Scope)
[InternalEvent(false)]
procedure OnInternalValidationCompleted(DocumentNo: Code[20])
begin
end;
// 4. Isolated Event Publisher
[IntegrationEvent(false, false, true)]
procedure OnAfterBroadcastNotification(NotificationId: Guid; MessageText: Text[250])
begin
end;
procedure Execute(var SalesHeader: Record "Sales Header")
var
IsHandled: Boolean;
begin
// Raising the integration event
OnBeforeProcessCustomOrder(SalesHeader, IsHandled);
if IsHandled then
exit;
// Standard processing continues if not handled
PerformStandardProcessing(SalesHeader);
end;
local procedure PerformStandardProcessing(var SalesHeader: Record "Sales Header")
begin
// Base processing logic
end;
}
Comparing Event Publisher Attributes
| Attribute | Scope / Visibility | Purpose & Stability | Arguments |
|---|---|---|---|
[IntegrationEvent] | Global (all extensions). | General-purpose extension point across tables, pages, and codeunits. Can evolve across major releases. | IncludeSender: Boolean, GlobalVarAccess: Boolean, Isolated: Boolean |
[BusinessEvent] | Global (all extensions). | Formal business contract (e.g., invoice posted, payment released). Guarantees permanent signature stability. | IncludeSender: Boolean, Isolated: Boolean |
[InternalEvent] | Module-internal only. | Internal communication within the same extension package. Not visible to external apps unless granted via internalsVisibleTo. | IncludeSender: Boolean, Isolated: Boolean |
Publisher Arguments Explained
IncludeSender: Boolean: When set totrue, the publisher signature implicitly exposes asenderparameter of typeCodeunit "PublisherName"orRecord "TableName"to subscribers, granting access to the publishing instance's public methods.GlobalVarAccess: Boolean: Legacy setting allowing subscribers to read and modify global variables of the publishing object. Marked obsolete in modern AL development and restricted tofalsein cloud-compliant apps.Isolated: Boolean: When set totrue, the platform executes each subscriber in its own isolated transaction boundary. If one subscriber encounters an unhandled runtime error, the engine catches the error, rolls back only that subscriber's changes, logs the error, and proceeds to execute all remaining subscribers without failing the overall transaction.
2. Event Subscribers in AL
An event subscriber is a procedure decorated with the [EventSubscriber] attribute that listens for a specific event raised by an object in the system.
codeunit 50115 "Sales Post Subscriber"
{
EventSubscriberInstance = StaticAutomatic; // Default behavior
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', true, true)]
local procedure HandleOnAfterPostSalesDoc(
var SalesHeader: Record "Sales Header";
var GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line";
SalesShptHdrNo: Code[20];
SalesInvHdrNo: Code[20];
SalesCrMemoHdrNo: Code[20];
InvHdrBuffer: Record "Invoice Post. Buffer")
begin
if SalesInvHdrNo <> '' then
SendCustomInvoiceEmailNotification(SalesInvHdrNo);
end;
local procedure SendCustomInvoiceEmailNotification(InvoiceNo: Code[20])
begin
// Custom notification workflow
end;
}
[EventSubscriber] Parameter Breakdown
[EventSubscriber(ObjectType, ObjectId, EventName, ElementName, SkipOnMissingLicense, SkipOnMissingPermission)]
ObjectType: The AL object type publishing the event (ObjectType::Table,ObjectType::Page,ObjectType::Codeunit,ObjectType::Report,ObjectType::XMLport).ObjectId: The symbolic object identifier (e.g.,Codeunit::"Sales-Post"orDatabase::Customer).EventName: String literal naming the publisher procedure (e.g.,'OnAfterPostSalesDoc').ElementName: Target field name or control name when subscribing to table field triggers (OnAfterValidate) or page control triggers. Empty string''for object-level events.SkipOnMissingLicense: Iftrue, the runtime ignores this subscriber silently if the tenant license does not cover the publishing object, preventing runtime licensing crashes.SkipOnMissingPermission: Iftrue, the subscriber is skipped without error if the current user lacks read/write permissions to the publishing object or its underlying tables.
Static vs. Manual Event Subscribers
By default, codeunits have EventSubscriberInstance = StaticAutomatic, meaning all subscribers inside the codeunit are bound automatically upon tenant startup. However, developers can configure manual binding:
EventSubscriberInstance = Manual: The codeunit's subscribers are inactive by default.BindSubscription(CodeunitInstance): Dynamically activates the subscribers on the specific codeunit instance at runtime.UnbindSubscription(CodeunitInstance): Deactivates the subscribers.- Use Cases: Critical for automated testing (binding mock subscribers during specific test functions) and transient workflow interception.
3. Standard Platform Trigger Events & The IsHandled Pattern
Built-in Table and Page Trigger Events
Every table and page in Business Central emits standard platform trigger events automatically without requiring manual publisher code:
- Table Trigger Events:
OnBeforeInsertEvent,OnAfterInsertEvent,OnBeforeModifyEvent,OnAfterModifyEvent,OnBeforeDeleteEvent,OnAfterDeleteEvent,OnBeforeRenameEvent,OnAfterRenameEvent,OnBeforeValidateEvent,OnAfterValidateEvent. - Trigger Event Parameters: Built-in table events provide
var Rec: Record TableName,var xRec: Record TableName, andRunTrigger: Boolean. - Page Trigger Events:
OnBeforeOpenPageEvent,OnAfterOpenPageEvent,OnClosePageEvent,OnQueryClosePageEvent,OnBeforeValidateEvent,OnAfterValidateEvent.
The IsHandled Design Pattern
The IsHandled pattern allows an extension to completely replace standard Business Central calculations, document routing, or posting routines without modifying core objects.
// 1. Core Base Application Publisher Pattern
procedure CalculateTax(var SalesLine: Record "Sales Line")
var
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeCalculateTax(SalesLine, IsHandled);
if IsHandled then
exit;
// Standard internal tax engine executes here
SalesLine."VAT %" := 20.0;
SalesLine."Amount Including VAT" := SalesLine.Amount * 1.2;
end;
// 2. Extension Subscriber Overriding Standard Behavior
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Tax Management", 'OnBeforeCalculateTax', '', false, false)]
local procedure OverrideWithThirdPartyTaxEngine(var SalesLine: Record "Sales Line"; var IsHandled: Boolean)
var
ExternalTaxService: Codeunit "External Tax Service";
begin
if IsHandled then
exit; // Another subscriber already handled this operation
SalesLine."VAT %" := ExternalTaxService.GetCalculatedRate(SalesLine."No.", SalesLine."Tax Group Code");
SalesLine."Amount Including VAT" := SalesLine.Amount * (1 + (SalesLine."VAT %" / 100));
IsHandled := true; // Suppresses standard base tax calculation
end;
Exam Watchout — Execution Order Non-Determinism: If multiple extensions subscribe to the exact same event publisher, the execution order of the subscribers is non-deterministic. Always check
if IsHandled then exit;at the start of your subscriber procedure to ensure you do not overwrite another extension's handling inadvertently.
A developer writes an event subscriber to integrate with an optional third-party ISV module. If the customer does not have a license for the third-party ISV codeunit, the subscriber must fail silently instead of throwing a runtime licensing error. How should the developer configure the [EventSubscriber] attribute?
When utilizing the 'IsHandled' design pattern to replace a standard base application calculation with a custom tax engine, what action must the subscriber take?
What is the key functional difference between [IntegrationEvent] and [BusinessEvent] attributes in AL?
A developer needs an event subscriber codeunit to remain inactive during normal user operations, but be dynamically activated and deactivated during specific automated test execution. Which codeunit property and AL methods should be used?