Getting Started with AL Extensions in Business Central
Microsoft Dynamics 365 Business Central is a powerful ERP platform, and its extensibility model — powered by the AL programming language — is what makes it genuinely adaptable to real business needs. In this post I'll walk through everything you need to start building AL extensions from scratch.
What Is an AL Extension?
An AL extension is a compiled package (.app file) that extends or customizes Business
Central's behavior without modifying the base application. This is the official, supported
path — as opposed to the C/AL modifications that existed in classic Dynamics NAV.
Extensions are:
- Upgrade-safe — they don't break when BC receives platform updates
- Isolated — they can be cleanly installed, uninstalled, or rolled back
- Publishable — to Microsoft AppSource or directly to your own tenant
Setting Up Your Environment
You'll need three things:
- VS Code + the AL Language extension
- Docker Desktop — for a local Business Central sandbox container
- BcContainerHelper — a PowerShell module that makes container management easy
# Install BcContainerHelper
Install-Module BcContainerHelper -Force
# Spin up a BC sandbox container
New-BcContainer `
-accept_eula `
-containerName "bcdev" `
-artifactUrl (Get-BcArtifactUrl -type sandbox -country base) `
-auth NavUserPassword `
-Credential (New-Object PSCredential "admin", `
(ConvertTo-SecureString "P@ssword1" -AsPlainText -Force))
Once the container is running, open VS Code and use Ctrl+Shift+P → AL: Go! to scaffold a new extension project and connect it to your container.
Project Structure
A minimal AL extension looks like this:
my-extension/
├── app.json ← manifest: ID, name, version, dependencies
├── .alpackages/ ← downloaded symbol packages
└── src/
├── TableExt.al
├── PageExt.al
└── Codeunit.al
app.json
{
"id": "12345678-1234-1234-1234-123456789012",
"name": "My First Extension",
"publisher": "Sujan Rai",
"version": "1.0.0.0",
"platform": "22.0.0.0",
"application": "22.0.0.0",
"idRanges": [{ "from": 50100, "to": 50199 }],
"dependencies": []
}
Always define an idRanges block. Object IDs must be unique within your tenant — using a per-publisher range avoids collisions.
Your First Table Extension
Let's add a custom field to the Customer table:
tableextension 50100 "Customer Ext." extends Customer
{
fields
{
field(50100; "Loyalty Points"; Integer)
{
Caption = 'Loyalty Points';
DataClassification = CustomerContent;
}
field(50101; "Loyalty Tier"; Enum "Loyalty Tier")
{
Caption = 'Loyalty Tier';
DataClassification = CustomerContent;
}
}
}
DataClassification is mandatory — Business Central enforces GDPR classification on all fields.
Page Extension
Expose the new fields on the Customer Card:
pageextension 50100 "Customer Card Ext." extends "Customer Card"
{
layout
{
addafter("Credit Limit (LCY)")
{
group(Loyalty)
{
Caption = 'Loyalty';
field("Loyalty Points"; Rec."Loyalty Points")
{
ApplicationArea = All;
ToolTip = 'Specifies the customer loyalty points balance.';
}
field("Loyalty Tier"; Rec."Loyalty Tier")
{
ApplicationArea = All;
ToolTip = 'Specifies the customer loyalty tier.';
}
}
}
}
}
Business Logic in a Codeunit
Keep your logic out of pages — put it in a codeunit:
codeunit 50100 "Loyalty Points Mgt."
{
procedure AddPoints(CustomerNo: Code[20]; Points: Integer)
var
Customer: Record Customer;
begin
if not Customer.Get(CustomerNo) then
Error('Customer %1 not found.', CustomerNo);
Customer."Loyalty Points" += Points;
UpdateTier(Customer);
Customer.Modify(true);
end;
local procedure UpdateTier(var Customer: Record Customer)
begin
case true of
Customer."Loyalty Points" >= 10000:
Customer."Loyalty Tier" := Customer."Loyalty Tier"::Platinum;
Customer."Loyalty Points" >= 5000:
Customer."Loyalty Tier" := Customer."Loyalty Tier"::Gold;
Customer."Loyalty Points" >= 1000:
Customer."Loyalty Tier" := Customer."Loyalty Tier"::Silver;
else
Customer."Loyalty Tier" := Customer."Loyalty Tier"::Bronze;
end;
end;
}
Key Patterns
Filtering and FINDSET
var
Customer: Record Customer;
begin
Customer.SetRange(Blocked, Customer.Blocked::" ");
Customer.SetFilter("Loyalty Points", '>%1', 500);
if Customer.FindSet() then
repeat
// process Customer
until Customer.Next() = 0;
end;
Subscribing to Events
Instead of modifying base app code, subscribe to published events:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', false, false)]
local procedure OnAfterSalesPost(var SalesHeader: Record "Sales Header")
var
LoyaltyMgt: Codeunit "Loyalty Points Mgt.";
begin
if SalesHeader."Document Type" = SalesHeader."Document Type"::Invoice then
LoyaltyMgt.AddPoints(SalesHeader."Sell-to Customer No.", 100);
end;
Publishing
Ctrl+Shift+P → AL: Publish Without Debugging (fast, no debugger attached)
F5 (publish + attach debugger)
What to Explore Next
Once you're comfortable with table and page extensions:
- API pages — expose BC data as REST endpoints with
PageType::API - Report extensions — customize RDLC layout and dataset
- Integration events — build loosely-coupled integrations between extensions
- Azure Functions + BC webhooks — event-driven external integrations
AL development has a learning curve, but the extensibility model is clean and disciplined once it clicks. The official AL docs are your best reference.
Feel free to reach out if you have questions — I'm always happy to talk BC development.