Al Extension Patterns

Production engineering guide for al extension patterns covering patterns, implementation strategies, and operational best practices.

Al Extension Patterns

In Microsoft Dynamics 365 Finance and Operations, AL (Application Language) extensions are the primary mechanism for customising and extending the core business logic. These extensions—typically deployed as .app packages—enable developers to add new tables, pages, codeunits, and workflows without modifying the base application. This approach is essential when building large-scale enterprise systems with layered functionality, where custom logic must coexist with updates, migrations, and integrations. Use AL extensions whenever you need to evolve a D365 FO system beyond its out-of-the-box capabilities—whether for industry-specific features, regulatory compliance, or integration with external platforms.

Core Patterns for AL Extension Development

1. Versioned Extension Deployment with Dependency Management

Extensions are versioned using a app.json file at the root of the extension project. Each extension declares its name, version, and dependencies. When deploying, the system resolves dependencies and applies updates in a consistent, atomic fashion.

{
  "name": "MyCompany.FinancialReporting",
  "version": "2.1.3",
  "publisher": "MyCompany",
  "description": "Advanced reporting for financial close.",
  "dependencies": [
    {
      "name": "Microsoft.Dynamics.Nav.App",
      "version": "10.0.0.0"
    },
    {
      "name": "MyCompany.InventoryExtensions",
      "version": "1.0.0.0"
    }
  ],
  "resources": [
    "Resources/MyCompany_FinancialReporting_2.1.3.resx"
  ]
}

The versioning scheme follows semantic versioning (SemVer), and the system enforces dependency compatibility at deployment time. If a dependent extension is not available, the deployment fails with a clear error:

Error: Dependency 'MyCompany.InventoryExtensions' not found.
Expected version: 1.0.0.0
Available versions: [0.9.0.0, 1.1.2.0, 2.0.0.0]

A common failure occurs when a new version of a base extension introduces breaking changes but the dependent extension does not specify a compatible version. This leads to silent runtime errors, where a page loads but fields are missing or codeunits fail with InvalidMethodCallException.

To avoid this, always use app.json to explicitly declare version ranges:

{
  "name": "MyCompany.Payroll",
  "version": "3.4.0",
  "dependencies": [
    {
      "name": "Microsoft.Dynamics.Nav.App",
      "version": "[10.0.0.0, 11.0.0.0)"
    }
  ]
}

The [10.0.0.0, 11.0.0.0) range allows any version from 10.0.0.0 up to but not including 11.0.0.0. This ensures backward compatibility while enabling gradual rollouts.

2. Custom Table Extensions with Field and Index Overlays

Extending a base table is a frequent task. AL allows adding new fields, modifying existing ones, and defining new indexes. The syntax is declarative and self-contained.

tableextension 50100 "MyCompany_PaymentTermsExt" extends "Payment Terms"
{
    fields
    {
        field(50100; "Max Days for Early Payment"; Integer)
        {
            Caption = 'Max Days for Early Payment';
            MinValue = 0;
            MaxValue = 365;
            DataClassification = CustomerContent;
        }
        field(50101; "Early Payment Discount %"; Decimal)
        {
            Caption = 'Early Payment Discount %';
            DecimalPlaces = 2:2;
            ValidateOnChange = true;
        }
    }

    keys
    {
        key(Primary; "Code")
        {
            Clustered = true;
        }
        key("Payment Term Name"; "Name")
        {
            Enabled = false;
        }
        key("Max Days"; "Max Days for Early Payment")
        {
            MaintainSIFT = true;
        }
    }
}

When the extension is deployed, the system automatically applies the changes to the underlying SQL Server database. The Clustered = true option ensures the primary key is clustered, improving query performance. The MaintainSIFT = true flag enables automatic index maintenance during data modifications.

A subtle failure mode arises when multiple extensions modify the same table. If two extensions define a field with the same FieldNo, the second extension overwrites the first. To prevent this, always use a consistent naming convention and reserve FieldNo ranges per extension.

Additionally, DataClassification is used by the system to determine data handling policies. When a field is marked CustomerContent, the system applies retention, auditing, and sensitivity labeling rules across the entire enterprise system.

3. Page Extension Patterns for User Experience Consistency

Extending pages—especially list and card pages—is critical for aligning custom functionality with user expectations. AL supports adding new controls, modifying existing ones, and defining new actions.

pageextension 50101 "MyCompany_SalesOrderExt" extends "Sales Order"
{
    layout
    {
        modify("Sell-to Customer No.")
        {
            Visible = true;
            Enabled = true;
            Importance = Promoted;
        }

        addafter("Sell-to Customer No.")
        {
            group("Payment Information")
            {
                Caption = 'Payment Information';
                Visible = "Payment Terms Code" <> '';

                field("Payment Date"; Rec."Payment Date")
                {
                    ApplicationArea = All;
                    ToolTip = 'Specifies the date the payment is due.';
                }

                field("Payment Method Code"; Rec."Payment Method Code")
                {
                    ApplicationArea = All;
                    Visible = false;
                }

                repeater(Control1)
                {
                    ShowCaption = false;

                    field("Payment Status"; Rec."Payment Status")
                    {
                        ApplicationArea = All;
                        Style = Strong;
                        StyleExpr = Rec."Payment Status" = Rec."Payment Status"::PartiallyPaid;
                    }
                }
            }
        }

        addlast(Sections)
        {
            part("Detailed Payment Lines"; "MyCompany Payment Lines")
            {
                SubPageLink = "Document No." = field("No.");
                SubPageView = sorting("Line No.") order(ascending);
            }
        }
    }

    actions
    {
        addlast(processing)
        {
            action("Calculate Early Payment Discount")
            {
                ApplicationArea = All;
                Caption = 'Calculate Early Payment Discount';
                Promoted = true;
                PromotedCategory = Process;

                trigger OnAction()
                begin
                    // Custom logic to calculate discount based on new fields
                    Rec.CalculateDiscount();
                end;
            }
        }
    }
}

The SubPageLink and SubPageView properties enable seamless integration of related data. The ApplicationArea property controls visibility based on user roles and permissions. The Promoted and PromotedCategory flags determine where the action appears in the ribbon.

A common failure occurs when the SubPageLink is not properly aligned with the target page’s key fields. If the linking field is not unique or not indexed, the sub-page loads slowly or fails to display data. The SubPageView property must be carefully crafted: sorting("Line No.") order(ascending) ensures consistent rendering, but missing order leads to unpredictable display order.

4. Event-Driven Integration via Codeunits and Triggers

AL enables event-based integration between systems. Codeunits are used to encapsulate business logic and can be triggered by events such as OnInsert, OnModify, or custom events.

codeunit 50102 "MyCompany_PaymentProcessor"
{
    trigger OnRun()
    begin
        // Entry point for batch processing
    end;

    [EventSubscriber(ObjectType::Table, Database::"Sales Invoice Header", 'OnInsert', '', true, true)]
    local procedure OnSalesInvoiceInsert(var Rec: Record "Sales Invoice Header")
    begin
        if Rec."Payment Terms Code" = 'EARLY10' then
            Rec."Payment Date" := CalcDate('<+10D>', Rec."Posting Date");
    end;

    [EventSubscriber(ObjectType::Page, Page::"Sales Order", 'OnAfterGetRecord', '', true, true)]
    local procedure OnSalesOrderAfterGetRecord(var Rec: Record "Sales Order")
    begin
        Rec."Payment Status" := GetPaymentStatus(Rec."No.");
    end;

    local procedure CalculateDiscount(InvoiceNo: Code[20]): Decimal
    begin
        // Implementation
    end;
}

The [EventSubscriber] attribute defines a method that runs when a specified event is fired. The ObjectType parameter can be Table, Page, Query, or Codeunit. The ObjectID is the table or page ID, and the EventName is the event name (e.g., OnInsert, OnAfterGetRecord).

A frequent failure is the incorrect use of true and false in the EventSubscriber parameters. The var Rec parameter is passed by reference, so changes to Rec are visible to the caller. The true, true in the decorator means the event is global (runs in all sessions) and synchronous (waits for completion before continuing).

When multiple subscribers exist for the same event, the order of execution is determined by the codeunit’s Run priority, which defaults to 100. A subscriber with Priority = 10 runs before one with Priority = 20, but the order is not guaranteed unless explicitly defined.

5. Cross-Extension Data Synchronization via Jobs and Background Processing

For complex integrations, data must be synchronised across multiple extensions. AL supports background processing via jobs and the Job Queue system.

codeunit 50103 "MyCompany_DataSyncJob"
{
    trigger OnRun()
    begin
        SyncSalesOrders();
        SyncInventoryItems();
        LogSyncResults();
    end;

    local procedure SyncSalesOrders()
    var
        SalesHeader: Record "Sales Header";
        JobQueueEntry: Record "Job Queue Entry";
    begin
        SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order);
        SalesHeader.SetRange("External Document No.", '', true); // Not synced yet

        if SalesHeader.FindSet() then
            repeat
                // Perform integration logic
                SyncToExternalSystem(SalesHeader);
            until SalesHeader.Next() = 0;
    end;

    local procedure LogSyncResults()
    begin
        JobQueueEntry."Job Queue Category Code" := 'DATA_SYNC';
        JobQueueEntry."Object Type to Run" := JobQueueEntry."Object Type to Run"::Codeunit;
        JobQueueEntry."Object ID to Run" := 50103;
        JobQueueEntry.Description := 'Sync sales data to external system';
        JobQueueEntry.Insert();
    end;
}

The Job Queue Entry table is used to schedule and track jobs. When a job is scheduled, it runs asynchronously and can be monitored via the Job Queue page. A job fails silently if an unhandled exception occurs in the OnRun trigger.

A common failure is forgetting to set the Job Queue Category Code. Without it, jobs are not picked up by the job runner. Additionally, the Job Queue Entry must be inserted before the job starts processing. If the Insert call is omitted, the job appears in the list but never runs.

When a job fails, the Job Queue Entry is updated with Error Message and Error Text. The error text often contains a stack trace and the full exception details, which can be retrieved via JobQueueEntry.GetErrorText().

Common Pitfalls and Silent Failures

These patterns, when applied consistently, enable robust, maintainable extensions that scale with the enterprise.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.