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

Getting Started with AL for Dynamics 365 Business Central

Kery Nguyen
By Kery Nguyen

2024-06-15

This guide shares what I wish I'd known from the start: practical AL insights from someone who learned through trial, error, and occasionally throwing things at my monitor.

AL Language: What It Actually Is (Beyond the Marketing Speak)

AL isn't just "a programming language for Business Central." It's Microsoft's replacement for the old C/AL language, redesigned around modern extension-based architecture rather than direct code modifications.

In practical terms, AL is:

  • A C#-like language with some Pascal-inspired syntax
  • Exclusively for extending Business Central (not a general-purpose language)
  • Designed to create packages that layer on top of base functionality
  • Limited to interacting with Business Central objects and APIs

What makes AL unusual is its tight coupling with Business Central's application objects. You're not building standalone applications—you're creating extensions that hook into existing tables, pages, and codeunits through a publisher-subscriber model.

This fundamental difference tripped me up constantly when I started. I kept trying to build things from scratch instead of extending what was already there.

Setting Up Your AL Development Environment That Actually Works

Skip the frustration of a broken setup with this tested approach:

1. The Essential Tools

  • Visual Studio Code: Your primary editor (free from code.visualstudio.com)
  • AL Language Extension: Provides AL syntax highlighting and compilation
  • GitLens Extension: Not mandatory but incredibly helpful for version control
  • Microsoft .NET Framework 4.8: Required for the compiler

2. The Two Environment Options

You have two choices for where your code will actually run:

Option A: Docker Container (Local Development)

  • Pros: Faster compile times, works offline, free to use
  • Cons: Complex setup, version management headaches
  • Best for: Daily development work

Option B: Business Central Sandbox (Cloud Development)

  • Pros: No local setup, always current version, matches production
  • Cons: Slower compile times, requires internet connection
  • Best for: Testing and final validation

After trying both, I settled on a hybrid approach: Docker for daily coding with periodic deployment to a sandbox for integration testing.

3. Setting Up Docker (The Simplified Way)

  1. Install Docker Desktop from docker.com
  2. Run this PowerShell script to pull and run the BC container:
$artifactUrl = Get-BcArtifactUrl -type OnPrem -version 22.0 -country us
$containerName = "BC220"

New-BcContainer `
    -accept_eula `
    -artifactUrl $artifactUrl `
    -auth NavUserPassword `
    -containerName $containerName `
    -licenseFile "C:\path\to\your\license.flf" `
    -updateHosts
  1. In VS Code, create a new AL project with:
    • Press F1 -> Type "AL: Go" -> Select "AL: Go!"
    • Choose "Your own server"
    • Enter "http://BC220" for server URL
    • Use "admin" for username and whatever password you set in your script

Pro Tip: Save a working container as a Docker image for quick restarts without reinstallation.

The AL Basics You'll Actually Use Every Day

Rather than a comprehensive language reference, here are the AL constructs I use in virtually every project:

Data Types You'll Use Most

TypeWhen to UseExample
TextFor descriptions, names, etc.Text[50] for fields that store short text
CodeFor IDs, numbers, or codesCode[20] for item numbers, much faster than Text for keys
DecimalFor money and quantitiesDecimal for any numeric value that might have decimals
BooleanFor yes/no flagsBoolean for simple toggles like "Approved"
EnumFor status fields with fixed valuesPerfect for status fields with defined options

Data type tip: Use Code instead of Text for any field you'll search on frequently. Business Central optimizes Code fields for lookups in ways it doesn't for Text.

The Control Structures Worth Remembering

While AL supports standard programming constructs, these patterns appear most frequently:

Conditional Shorthand

if Customer."Credit Limit" < 1000 then
    Customer."Credit Limit" := 1000;

For Each Loop (Used Constantly)

foreach Customer in Customers do begin
    // Do something with each customer
    Customer.CalcFields("Balance Due");
    TotalBalance += Customer."Balance Due";
end;

Case Statement for Status Fields

case SalesHeader.Status of
    SalesHeader.Status::Open:
        CanEdit := true;
    SalesHeader.Status::Released:
        CanEdit := false;
    SalesHeader.Status::"Pending Approval":
        CanEdit := false;
end;

Early Returns for Validation

procedure IsValid(): Boolean
begin
    if not HasRequired() then
        exit(false);
    if Amount <= 0 then
        exit(false);
    exit(true);
end;

Event Subscribers: The Heart of AL Development

Event subscribers let your code run when specific actions happen in Business Central. These are absolutely essential:

Table Events (Most Common)

[EventSubscriber(ObjectType::Table, Database::"Sales Header", 'OnAfterInsertEvent', '', false, false)]
local procedure OnAfterInsertSalesHeader(var Rec: Record "Sales Header")
begin
    // Runs after a new sales header is created
    Rec.Validate("Your Custom Field", 'Default Value');
    Rec.Modify();
end;

Page Events (For UI Logic)

[EventSubscriber(ObjectType::Page, Page::"Customer Card", 'OnAfterGetCurrRecord', '', false, false)]
local procedure OnAfterGetCurrRecordCustomerCard(var Rec: Record Customer)
begin
    // Runs when a user views a customer record
    RefreshDashboard(Rec."No.");
end;

Codeunit Events (For Business Logic)

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', false, false)]
local procedure OnAfterPostSalesDoc(var SalesHeader: Record "Sales Header"; var GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line"; ...)
begin
    // Runs after a sales document is posted
    CreateCustomFollow(SalesHeader);
end;

Pro tip: Always use local procedures for event subscribers with clear names describing what they do. This makes debugging much easier later.

Building Your First Useful AL Extension

Let's build something practical—a customer credit limit approval extension that adds a workflow for credit limit changes. This covers the core AL development patterns you'll use repeatedly:

Step 1: Define Your Table Extensions

First, extend the Customer table to add fields for the approval workflow:

tableextension 50100 "Credit Limit Ext" extends Customer
{
    fields
    {
        field(50100; "Credit Limit Requested"; Decimal)
        {
            Caption = 'Credit Limit Requested';
            DataClassification = CustomerContent;
        }
        field(50101; "Credit Limit Status"; Enum "Credit Limit Status")
        {
            Caption = 'Credit Limit Status';
            DataClassification = CustomerContent;
        }
        field(50102; "Credit Limit Requested By"; Code[50])
        {
            Caption = 'Requested By';
            DataClassification = CustomerContent;
            TableRelation = "User Setup";
            Editable = false;
        }
    }
}

Don't forget to create the enum for status:

enum 50100 "Credit Limit Status"
{
    Extensible = true;
    
    value(0; Open) { Caption = 'Open'; }
    value(1; "Pending Approval") { Caption = 'Pending Approval'; }
    value(2; Approved) { Caption = 'Approved'; }
    value(3; Rejected) { Caption = 'Rejected'; }
}

Step 2: Extend the Customer Card Page

Now add these fields to the Customer Card:

pageextension 50100 "Credit Limit Card Ext" extends "Customer Card"
{
    layout
    {
        addafter("Credit Limit (LCY)")
        {
            field("Credit Limit Requested"; Rec."Credit Limit Requested")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the requested credit limit pending approval.';
                Editable = CanEditRequested;
            }
            field("Credit Limit Status"; Rec."Credit Limit Status")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the status of the credit limit request.';
                Editable = false;
            }
        }
    }
    
    actions
    {
        addfirst(processing)
        {
            action(RequestApproval)
            {
                ApplicationArea = All;
                Caption = 'Request Credit Approval';
                Image = SendApprovalRequest;
                ToolTip = 'Send the credit limit change for approval.';
                
                trigger OnAction()
                begin
                    RequestCreditApproval();
                end;
            }
        }
    }
    
    var
        CanEditRequested: Boolean;
        
    trigger OnAfterGetRecord()
    begin
        SetEditableControls();
    end;
    
    local procedure SetEditableControls()
    begin
        CanEditRequested := Rec."Credit Limit Status" = Rec."Credit Limit Status"::Open;
    end;
    
    local procedure RequestCreditApproval()
    begin
        if Rec."Credit Limit Requested" <= 0 then begin
            Error('Please specify a requested credit limit amount.');
            exit;
        end;
        
        Rec.Validate("Credit Limit Status", Rec."Credit Limit Status"::"Pending Approval");
        Rec.Validate("Credit Limit Requested By", UserId);
        Rec.Modify();
        Message('Approval request sent.');
    end;
}

Step 3: Create Approval Codeunit

Next, create a codeunit to handle the approvals:

codeunit 50100 "Credit Limit Approval Mgmt"
{
    procedure ApproveRequest(CustomerNo: Code[20])
    var
        Customer: Record Customer;
    begin
        if Customer.Get(CustomerNo) then begin
            Customer.Validate("Credit Limit (LCY)", Customer."Credit Limit Requested");
            Customer.Validate("Credit Limit Status", Customer."Credit Limit Status"::Approved);
            Customer.Modify();
        end;
    end;
    
    procedure RejectRequest(CustomerNo: Code[20])
    var
        Customer: Record Customer;
    begin
        if Customer.Get(CustomerNo) then begin
            Customer.Validate("Credit Limit Status", Customer."Credit Limit Status"::Rejected);
            Customer.Modify();
        end;
    end;
}

Step 4: Create an Approval List Page

Finally, create a page for the approvals:

page 50100 "Credit Limit Approvals"
{
    PageType = List;
    SourceTable = Customer;
    Caption = 'Credit Limit Approval Requests';
    CardPageId = "Customer Card";
    SourceTableView = where("Credit Limit Status" = const("Pending Approval"));
    
    layout
    {
        area(Content)
        {
            repeater(Requests)
            {
                field("No."; Rec."No.") { ApplicationArea = All; }
                field(Name; Rec.Name) { ApplicationArea = All; }
                field("Credit Limit (LCY)"; Rec."Credit Limit (LCY)") { ApplicationArea = All; }
                field("Credit Limit Requested"; Rec."Credit Limit Requested") { ApplicationArea = All; }
                field("Credit Limit Requested By"; Rec."Credit Limit Requested By") { ApplicationArea = All; }
            }
        }
    }
    
    actions
    {
        area(Processing)
        {
            action(Approve)
            {
                ApplicationArea = All;
                Caption = 'Approve';
                Image = Approve;
                
                trigger OnAction()
                var
                    CreditApproval: Codeunit "Credit Limit Approval Mgmt";
                begin
                    CreditApproval.ApproveRequest(Rec."No.");
                    CurrPage.Update(false);
                end;
            }
            
            action(Reject)
            {
                ApplicationArea = All;
                Caption = 'Reject';
                Image = Reject;
                
                trigger OnAction()
                var
                    CreditApproval: Codeunit "Credit Limit Approval Mgmt";
                begin
                    CreditApproval.RejectRequest(Rec."No.");
                    CurrPage.Update(false);
                end;
            }
        }
    }
}

With this simple extension, you've added:

  • Custom fields to existing tables
  • UI elements to existing pages
  • Business logic in a codeunit
  • A completely new page for approvals

AL Debugging Techniques That Actually Save Time

The difference between an amateur and professional AL developer often comes down to debugging skill. Here's what works:

1. Strategic Message Debugging

When things aren't working, add strategic message calls:

procedure ProcessSomething()
var
    Customer: Record Customer;
begin
    Message('Starting process for %1 customers', Customer.Count());
    
    if Customer.FindSet() then
        repeat
            Message('Processing customer %1', Customer."No.");
            // Your code here
        until Customer.Next() = 0;
    
    Message('Process complete');
end;

2. Using the Event Recorder

Business Central's Event Recorder captures precisely which events fire as you use the system:

  1. Open Business Central web client
  2. Press Ctrl+Alt+F1
  3. Select "Event Recorder"
  4. Perform the actions you're trying to extend
  5. See exactly which events fire in what order

This is invaluable for finding the right event to subscribe to.

3. AL Object Browser

VS Code's AL Object Browser shows you exactly what fields, methods, and events exist:

  1. In VS Code, click the "AL Object Browser" icon in the sidebar
  2. Search for the table or page you're extending
  3. Expand to see all fields, methods, and properties
  4. Right-click to insert references or field lists

Pro tip: The field tooltips show the exact data type and properties, saving countless trips to Microsoft docs.

Real-World AL Patterns That Solve Actual Problems

These patterns come from real extensions I've built and deployed:

Pattern 1: Safe Field Edits

When updating fields, wrap in a validate-modify pattern:

procedure UpdateCustomerEmail(CustomerNo: Code[20]; NewEmail: Text[80])
var
    Customer: Record Customer;
begin
    if not Customer.Get(CustomerNo) then
        exit;
        
    if Customer."E-Mail" <> NewEmail then begin
        Customer.Validate("E-Mail", NewEmail);
        Customer.Modify(true);
    end;
end;

Pattern 2: Check-Insert-Modify

When creating records that might already exist:

procedure EnsureRecordExists(ItemNo: Code[20]; LocationCode: Code[10])
var
    ItemLocation: Record "Item Location";
begin
    ItemLocation.SetRange("Item No.", ItemNo);
    ItemLocation.SetRange("Location Code", LocationCode);
    
    if not ItemLocation.FindFirst() then begin
        // Insert new
        ItemLocation.Init();
        ItemLocation."Item No." := ItemNo;
        ItemLocation."Location Code" := LocationCode;
        ItemLocation.Insert(true);
    end else begin
        // Update existing if needed
        if ItemLocation.Status <> ItemLocation.Status::Active then begin
            ItemLocation.Status := ItemLocation.Status::Active;
            ItemLocation.Modify(true);
        end;
    end;
end;

Pattern 3: Using Temporary Tables for Processing

For complex calculations without database impact:

procedure CalculateDiscounts()
var
    SalesLine: Record "Sales Line";
    TempDiscount: Record "Sales Line" temporary;
    Item: Record Item;
begin
    if SalesLine.FindSet() then
        repeat
            // Skip to temp table for processing
            TempDiscount.Init();
            TempDiscount.TransferFields(SalesLine);
            
            // Do calculations
            if Item.Get(SalesLine."No.") then
                TempDiscount."Line Discount %" := CalculateItemDiscount(Item);
                
            TempDiscount.Insert();
        until SalesLine.Next() = 0;
    
    // Now process the temp table
    if TempDiscount.FindSet() then
        repeat
            // Update actual records
            SalesLine.Get(TempDiscount."Document Type", TempDiscount."Document No.", TempDiscount."Line No.");
            SalesLine."Line Discount %" := TempDiscount."Line Discount %";
            SalesLine.Modify();
        until TempDiscount.Next() = 0;
end;

Common AL Mistakes I've Made (So You Don't Have To)

Learn from my pain:

Mistake 1: Trying to Modify System Tables Directly

What I did wrong: Tried to directly modify the Sales Header table in code without using events or proper API.

Why it's a problem: This breaks when Microsoft updates the base application.

Better approach: Use event subscribers to react to changes rather than making direct modifications.

Mistake 2: Ignoring Performance on Lists

What I did wrong: Created a list extension that ran expensive calculations on each record.

What happened: The page took 30+ seconds to load with just 100 records.

Better approach: Calculate values in batch processes and store results, or use FlowFields for calculated values.

Mistake 3: Hardcoding Business Rules

What I did wrong: Hardcoded approval thresholds and business rules directly in AL code.

What happened: Every rule change required a code change and redeployment.

Better approach: Create setup tables for configurable business rules that users can modify.

Deployment Strategies That Actually Work

Getting your code into production safely:

1. Environment Strategy

For any serious development:

  • Development: Your local Docker or personal sandbox
  • Test/QA: Shared sandbox for integration testing
  • Pre-Production: Mirror of production for final validation
  • Production: The live environment

2. Version Control Must-Haves

  • Use Git for all AL projects, even solo work
  • Create meaningful commit messages describing what changed
  • Use branches for features, hotfixes, and releases
  • Never commit secrets or connection details

3. App Package Versioning

Follow semantic versioning for your app:

1.0.0.0
│ │ │ │
│ │ │ └─ Build number (increment for minor fixes)
│ │ └─── Minor version (increment for new features)
│ └───── Major version (increment for breaking changes)
└─────── Major version (rarely changes)

Update the version in app.json with each release.

Taking Your AL Skills to the Next Level

Once you've mastered the basics, these advanced techniques separate good developers from great ones:

1. AL-Specific Design Patterns

  • Repository Pattern: Create dedicated codeunits for data access
  • Factory Pattern: Use codeunits to centralize object creation logic
  • Notification Pattern: Use the system notification library for user feedback

2. Integration Patterns

  • Web Services: Expose and consume APIs for external integration
  • Background Jobs: Use job queue entries for long-running processes
  • Azure Functions: Create AL wrappers for complex external processes

3. Code Quality Best Practices

  • Naming Conventions: Use descriptive, consistent names
  • Error Handling: Implement proper error handling and user feedback
  • Code Reviews: Have peers review your AL code before deployment

Final Thoughts: The AL Learning Journey

After five years of building AL extensions, I've learned that mastery doesn't come from memorizing syntax or studying documentation—it comes from building real solutions that solve actual business problems.

Start small. Build extensions that address specific needs in your organization. Learn from what works and what doesn't. Gradually tackle more complex challenges as your confidence grows.

The most valuable skill isn't knowing every AL function by heart—it's knowing how to translate business requirements into working code that integrates seamlessly with Business Central's existing functionality.

Business CentralAL LanguageSoftware DevelopmentExtensionsMicrosoft Dynamics 365
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

Sales Order and Invoicing Best Practices for SMBs

Learn how to streamline your sales order and invoicing processes with best practices, digital tools, and real-world tips. Ideal for SMB owners and financial managers.

By

Kery Nguyen

Date

2024-07-20

image

Audit Trails in Business Central for Data Integrity

Learn how to use audit trails in Microsoft Dynamics 365 Business Central to track changes, maintain data integrity, and ensure accountability across your business operations.

By

Kery Nguyen

Date

2024-07-01

image

Keyboard Shortcuts to Speed Up Business Central Tasks

Learn to navigate and use Microsoft Dynamics 365 Business Central with greater efficiency using our comprehensive guide to keyboard shortcuts. Perfect for financial professionals, managers, and project leaders aiming to boost productivity.

By

Kery Nguyen

Date

2024-06-29