12.1 AL Syntax, Variable Scope, Primitive & Complex Data Types
Key Takeaways
- AL is a strongly typed, case-insensitive language rooted in Pascal syntax, where statements terminate with semicolons, blocks are enclosed in begin...end, and assignments use the := operator.
- Code data types automatically convert alphanumeric characters to uppercase and strip leading and trailing whitespace, whereas Text data types preserve exact casing and spacing.
- Pass-by-reference using the var keyword passes the memory reference of a variable to a procedure, allowing direct mutation of the caller buffer without heap allocation.
- Modern AL collections (List of [T], Dictionary of [K, V]) and TextBuilder provide type-safe, high-performance in-memory manipulation that outperforms legacy arrays and repeated Text concatenations.
- RecordRef and FieldRef enable dynamic, late-bound table and field operations without compile-time schema coupling, while Guid and RecordID provide lightweight entity identifiers.
12.1 AL Syntax, Variable Scope, Primitive & Complex Data Types
The Application Language (AL) is the foundational programming language used to build extensions for Microsoft Dynamics 365 Business Central. Rooted in the Pascal programming paradigm inherited from C/AL, modern AL runs entirely in the cloud containerized runtime of Business Central. It compiles down to .NET Common Intermediate Language (CIL) executing on the Service Tier. For developers preparing for the MB-820 exam, mastering AL syntax conventions, variable lifecycles, memory scoping, primitive types, complex reflection objects, and modern generic collections is essential for building scalable, enterprise-grade business applications.
1. AL Language Structure & Syntax Conventions
AL is a strongly typed, case-insensitive programming language. While identifiers, keywords, and variable names are case-insensitive, adhering to Microsoft standard naming conventions (PascalCase for objects, methods, and variables) is strongly enforced by AL code analyzers (CodeCop).
// Object definition with explicit properties, variables, and procedure triggers
codeunit 50100 "Sales Processing Engine"
{
Access = Public;
Subtype = Normal;
trigger OnRun()
var
CustomerRec: Record Customer;
ProcessingSuccess: Boolean;
begin
ProcessingSuccess := ProcessCustomer(CustomerRec);
end;
local procedure ProcessCustomer(var Cust: Record Customer): Boolean
var
InitialCreditLimit: Decimal;
begin
InitialCreditLimit := Cust."Credit Limit (LCY)";
if InitialCreditLimit > 50000 then
exit(true);
exit(false);
end;
}
Core Syntax Rules & Conventions
- Statement Terminators: Every standalone executable statement must terminate with a semicolon (
;). - Block Delimiters: Code blocks containing multiple statements must be enclosed within
beginandendkeywords. A semicolon followsend;, except when preceding anelsekeyword (end else begin). - Assignment vs. Equality: The assignment operator is
:=(colon-equals), whereas the relational equality operator is=(single equals). Evaluatingif A = Btests equality; executingA := B;mutates valueA. - Comments: Single-line comments begin with
//. Multi-line block comments are enclosed within/*and*/. - Variable Declaration Sections: Variables cannot be declared inline. They must be declared within a dedicated
varblock immediately preceding thebeginkeyword of an object, trigger, or procedure. - Quoted Identifiers: Field, table, and variable names that contain spaces, special characters, or match reserved AL keywords must be wrapped in double quotes (e.g.,
"Credit Limit (LCY)","No."). - String Literals: Literal text strings and characters must be wrapped in single quotes (e.g.,
'Customer %1 is blocked.'). Single quotes within a string literal are escaped by doubling them ('John''s Order').
2. Primitive Data Types: Numbers, Booleans, and Text
AL provides a rich set of primitive data types designed specifically for transactional business processing, relational integrity, and high-precision financial accounting.
+---------------------------------------------------------------------------------------------------+
| AL PRIMITIVE DATA TYPES OVERVIEW |
+-------------------+--------------------+----------------------------------------------------------+
| Category | AL Data Type | Range / Storage Characteristics |
+-------------------+--------------------+----------------------------------------------------------+
| Integer Numerics | Integer | 32-bit signed: -2,147,483,648 to 2,147,483,647 |
| | BigInteger | 64-bit signed: -9,223,372,036,854,775,808 to 9,223,372... |
| | Byte | 8-bit unsigned: 0 to 255 |
| Financial Numeric | Decimal | 18-digit fixed-point precision with exact scale |
| Logical & Char | Boolean | true or false |
| | Char | Single 16-bit UTF-16 character code |
| Textual Strings | Text / Text[N] | Dynamic Unicode string or fixed-length buffer (up to 2GB)|
| Lookup Codes | Code / Code[N] | Auto-uppercase, whitespace-trimmed string (up to 2048 ch)|
+-------------------+--------------------+----------------------------------------------------------+
The Critical Difference: Code vs. Text
Understanding the behavioral distinction between Code and Text is one of the most frequently tested concepts on the MB-820 exam:
var
MyText: Text[30];
MyCode: Code[30];
begin
MyText := ' Cust-1001_abc ';
MyCode := ' Cust-1001_abc ';
// Resulting Values:
// MyText = ' Cust-1001_abc ' (Exact casing preserved, leading/trailing whitespace preserved)
// MyCode = 'CUST-1001_ABC' (Converted to UPPERCASE, leading and trailing whitespace stripped)
end;
Text/Text[N]: Preserves exact casing and surrounding whitespace. Used for descriptive content such as names, addresses, descriptions, JSON payloads, and unstructured comments.Code/Code[N]: Automatically converts all lowercase letters to UPPERCASE and strips leading and trailing whitespace upon assignment. Internal spaces are preserved. Used strictly for system keys, setup codes, primary keys ("No."), posting groups, and identifiers (e.g.,Customer."No.",Item."No.","Gen. Bus. Posting Group").
Detailed Comparison of Primitive Types
| Data Type | Memory & Range | Default Value | Exam Focus & Practical Best Practices |
|---|---|---|---|
Integer | 32-bit signed integer | 0 | Loop counters, line numbering increments (Line No. += 10000), table object IDs (Database::Customer). |
BigInteger | 64-bit signed integer | 0 | High-volume transaction entry sequences, ledger sequence IDs, timestamp counters. |
Decimal | 18-digit fixed-point | 0.0 | Mandatory for all financial and quantity calculations. Unlike floating-point types in other languages, AL Decimal avoids binary rounding anomalies. |
Boolean | 1-bit logical flag | false | Conditional toggles, validation flags, status checks. |
Char | 16-bit UTF-16 character | 0 (null char) | Single character inspection, ASCII/Unicode delimiter processing (e.g., CR := 13; LF := 10;). |
Byte | 8-bit unsigned integer | 0 | Binary stream manipulation, raw cryptographic byte arrays. |
3. Temporal, Identifier & Complex Data Types
In addition to basic primitives, Business Central incorporates specialized data types for time management, entity identification, and runtime reflection.
Temporal Types
Date: Represents a calendar date (e.g.,2026-08-29D). Does not contain a time component. Default value is0D(blank date). Supports literal formats and date formulas.Time: Represents the time of day with millisecond precision (e.g.,14:30:15.500T). Default value is0T(blank time).DateTime: Represents an absolute point in time stored in Coordinated Universal Time (UTC) (e.g.,2026-08-29T14:30:15.500Z). Default value is0DT.Duration: Represents an elapsed time interval in milliseconds stored as a 64-bit integer. Calculated by subtracting twoDateTimevalues (Elapsed := EndDateTime - StartDateTime;).DateFormula: Encapsulates a non-localized date calculation pattern (e.g.,'<1M+10D>','<-CY>').
System & Identifier Types
Guid: 128-bit Globally Unique Identifier ({d3a45678-1234-4a5b-8c9d-0123456789ab}). Used for integration primary keys (SystemId), telemetry trace correlation, and OAuth token tracking.RecordId: Contains the table number and the primary key field values of a specific record without loading the underlying table data buffer into memory. Lightweight and serializable.Enum: A strongly-typed, extensible list of named options that replaces legacyOptiontypes. Enums support interface implementations and modular AppSource extensions.
Complex & Reflection Types (RecordRef, FieldRef, Variant)
When building generic frameworks, data integration utilities, or audit engines, developers need to inspect and manipulate records dynamically at runtime without compile-time table bindings.
local procedure LogRecordChanges(RecVariant: Variant)
var
RecRef: RecordRef;
FldRef: FieldRef;
FieldIndex: Integer;
begin
// 1. Inspect and open record reference dynamically from Variant
if not RecVariant.IsRecord then
exit;
RecRef.GetTable(RecVariant);
// 2. Iterate through all fields defined on the table
for FieldIndex := 1 to RecRef.FieldCount do begin
FldRef := RecRef.FieldIndex(FieldIndex);
// Inspect field metadata and values dynamically
if FldRef.Class = FieldClass::Normal then
Message('Field %1 (%2) = %3', FldRef.Caption, FldRef.Number, FldRef.Value);
end;
RecRef.Close();
end;
RecordRef: A dynamic reference pointer to any record in any table in the database. Opened viaRecRef.Open(TableNo)orRecRef.GetTable(RecordVar).FieldRef: A dynamic reference pointer to a specific field within aRecordRef. Retrieved viaRecRef.Field(FieldNo)orRecRef.FieldIndex(Index).Variant: A weakly-typed container variable that can hold any AL data type, record, or object instance at runtime. Must be inspected using helper methods (Variant.IsRecord,Variant.IsInteger,Variant.IsCodeunit).
4. Modern Generic Collections & TextBuilder
Modern AL introduced strongly-typed generic collections and string builders to replace legacy arrays and avoid the performance penalties of immutable string concatenations.
List of [T] (Dynamic Strongly-Typed Collections)
The List of [T] type provides a dynamically resizing, ordered collection of elements of any primitive or complex type (e.g., List of [Text], List of [Guid], List of [Integer]).
local procedure ProcessCustomerCodes()
var
CustomerList: List of [Code[20]];
CustCode: Code[20];
begin
// 1. Adding elements
CustomerList.Add('CUST-001');
CustomerList.Add('CUST-002');
CustomerList.Add('CUST-003');
// 2. Querying list state
if CustomerList.Contains('CUST-002') then
Message('List count: %1', CustomerList.Count()); // 3
// 3. 1-Based Index Access
CustCode := CustomerList.Get(1); // Returns 'CUST-001'
// 4. Enumeration
foreach CustCode in CustomerList do begin
ProcessEntry(CustCode);
end;
// 5. Manipulation
CustomerList.Remove('CUST-002');
CustomerList.Reverse();
end;
Dictionary of [KeyType, ValueType] (Key-Value Hash Maps)
The Dictionary of [K, V] type provides fast key-value lookups using hash table indexing. Key types can be any primitive type (e.g., Code[20], Integer, Guid), and values can be primitive or complex types.
local procedure CalculateItemSales()
var
SalesSummary: Dictionary of [Code[20], Decimal];
ItemNo: Code[20];
TotalRevenue: Decimal;
begin
// Adding or Updating Key-Value Pairs
if not SalesSummary.ContainsKey('ITEM-A') then
SalesSummary.Add('ITEM-A', 1500.50)
else
SalesSummary.Set('ITEM-A', SalesSummary.Get('ITEM-A') + 1500.50);
// Safe Value Retrieval
if SalesSummary.Get('ITEM-A', TotalRevenue) then
Message('Total Revenue for ITEM-A: %1', TotalRevenue);
end;
Exam Watchout — Dictionary Safety: Attempting to call
Dict.Get(Key)on a key that does not exist in the dictionary throws a fatal runtime error: "The given key was not present in the dictionary." Always verify key existence usingDict.ContainsKey(Key)or use the safe retrieval patternDict.Get(Key, ValueVariable)before accessing entries.
TextBuilder (High-Performance String Manipulation)
In AL, standard Text variables are immutable. Repeatedly concatenating strings using TextVar += NewChunk inside a loop creates a new memory allocation on every iteration, leading to exponential memory usage and severe garbage collection thrashing on large datasets.
TextBuilder provides a mutable in-memory character buffer that optimizes string concatenation:
local procedure BuildLargeCsvExport(var ItemLedgerEntry: Record "Item Ledger Entry"): Text
var
CsvBuilder: TextBuilder;
begin
CsvBuilder.AppendLine('Entry No.,Item No.,Posting Date,Quantity');
if ItemLedgerEntry.FindSet() then
repeat
CsvBuilder.Append(Format(ItemLedgerEntry."Entry No."));
CsvBuilder.Append(',');
CsvBuilder.Append(ItemLedgerEntry."Item No.");
CsvBuilder.Append(',');
CsvBuilder.Append(Format(ItemLedgerEntry."Posting Date"));
CsvBuilder.Append(',');
CsvBuilder.AppendLine(Format(ItemLedgerEntry.Quantity));
until ItemLedgerEntry.Next() = 0;
exit(CsvBuilder.ToText());
end;
5. Variable Scoping, Lifecycles & Parameter Passing
Understanding memory lifecycle and parameter mechanics is critical for building robust AL procedures and preventing state corruption.
Scope & Lifecycle Matrix
| Variable Scope | Declaration Location | Lifetime & Storage | Visibility & Access Rules |
|---|---|---|---|
| Global Object Variable | Top-level var block of object | Instantiated when object loads; destroyed when object is unloaded from memory. In SingleInstance codeunits, persists for the entire client session. | Accessible by all triggers and procedures within the declaring object. Inaccessible from outside objects. |
| Local Procedure Variable | Procedure or trigger var block | Allocated on the call stack when the procedure is entered; deallocated immediately upon procedure exit. | Accessible only within that specific procedure or trigger. |
| Pass-by-Value Parameter | Procedure signature without var | Stack copy of caller's variable. | Changes to the parameter variable within the procedure do not affect the caller's original variable. |
| Pass-by-Reference Parameter | Procedure signature with var | Memory reference/pointer to the caller's variable. | Any modifications, filters, or record changes applied within the procedure directly mutate the caller's variable in memory. |
A developer assigns the string value ' item-990a ' to a variable declared as ItemCode: Code[20]. What is the exact value stored in ItemCode after the assignment statement ItemCode := ' item-990a '; executes?
A developer creates a procedure declared as: procedure UpdateCustomerCreditLimit(var Cust: Record Customer; NewLimit: Decimal). Inside the procedure, Cust."Credit Limit (LCY)" := NewLimit; Cust.Modify(); is executed. The calling codeunit passes a customer record variable CustomerRec into this procedure. What is the effect on CustomerRec in the calling codeunit?
An AL developer needs to assemble an export string containing 50,000 transaction lines from Item Ledger Entry. Which data type and approach should the developer use to achieve optimal execution performance and prevent severe memory thrashing?
A developer creates a lookup cache using PricesByItem: Dictionary of [Code[20], Decimal]. The dictionary currently contains entries for items 'ITEM01' and 'ITEM02'. What happens when the code executes UnitPrice := PricesByItem.Get('ITEM03'); without first verifying that the key exists?