AL Language Overview
Introduction
Erik's ToolBox implements a subset of the standard AL language used in Business Central. Unlike the standard development workflow, ToolBox compiles and interprets AL code at runtime. You do not need VS Code, a development environment, or app deployment to write and execute AL scripts.
This makes ToolBox ideal for quick automation, data manipulation, and administrative tasks directly within Business Central.
Program Structure
Every ToolBox script follows a straightforward structure consisting of three sections, in this order:
- Variables - Data storage declared in a
varblock - Procedures - Reusable blocks of logic
- Main body - The
begin/endblock that serves as the script entry point
A script starts directly with a var section (if there are variables) or begin (if there are no variables or procedures). When you execute a script, ToolBox runs the statements in the main begin/end block.
Basic Program Structure
var
Greeting: Text;
begin
Greeting := 'Hello from ToolBox!';
Message(Greeting);
end;
This is the simplest possible ToolBox script. When you run the script, ToolBox executes the statements between begin and end.
Program with Procedures
var
OriginalPrice: Decimal;
procedure CalculateDiscount(Amount: Decimal; DiscountPct: Decimal): Decimal
begin
exit(Amount * DiscountPct / 100);
end;
begin
OriginalPrice := 250.00;
Message('Discount: %1', CalculateDiscount(OriginalPrice, 10));
end;
When the script includes procedures, they are defined between the var block and the main begin/end block.
Data Types
ToolBox supports the following data types in your scripts.
Simple Types
| Type | Description |
|---|---|
| Integer | Whole numbers (32-bit) |
| BigInteger | Whole numbers with a much larger range (64-bit) |
| Decimal | Numbers with decimal places |
| Text | Variable-length string |
| Code | Uppercase, trimmed string |
| Boolean | True or False |
| Date | Calendar date (native value) |
| Time | Time of day (native value) |
| DateTime | Combined date and time (native value) |
| Guid | Globally unique identifier |
| Option | Enumerated value, declared with its members: Status: Option Open,Released,Closed; |
| Duration | Time span in milliseconds |
| DateFormula | Relative date calculation formula |
| Char | A single character (also usable as its integer code) |
| Byte | A single byte value (0..255) |
Text and Code accept an optional maximum length: Name: Text[100];, ItemNo: Code[20];. Assigning a longer value to a sized variable raises an overflow error rather than truncating, so use CopyStr when the source may be longer.
Dates, times, and datetimes are stored as native values, so they compare chronologically and support arithmetic (for example Date - Date yields a number of days). ToolBox also converts automatically between compatible types on assignment and comparison. See Types and Automatic Conversion.
Complex Types
| Type | Description |
|---|---|
| Record | Database table reference |
| Dialog | Progress dialog |
| array[N] of Type | Fixed-size, 1-based array |
| List of [Type] | Dynamic, 1-based list |
| Dictionary of [Key, Value] | Key/value dictionary |
| JsonObject / JsonArray / JsonToken / JsonValue | JSON values |
| InStream / OutStream | Streams for reading/writing data |
| Codeunit Subtype | A supported standard codeunit |
Types that exist in standard AL but cannot be declared in a ToolBox script include TextBuilder, Label, Enum, Interface, Variant, RecordRef, FieldRef, HttpClient, XmlDocument, Blob, and Page/Report/Query variables. See Limitations and Differences from Standard AL for the full list and the replacements to use.
Variable Declarations
You declare variables in a var block. Variables can be declared at the script level (before begin) or at the local level (inside a procedure).
Script Variables
Variables declared in the script's var block are accessible from the main body and from all procedures in the script.
var
Counter: Integer;
CustomerName: Text;
TotalAmount: Decimal;
IsActive: Boolean;
begin
Counter := 10;
CustomerName := 'Northwind Traders';
TotalAmount := 1500.75;
IsActive := true;
Message('Customer: %1, Amount: %2', CustomerName, TotalAmount);
end;
Procedure Variables
Procedures can declare their own local variables in a var block. These are only accessible within that procedure.
var
GlobalCounter: Integer;
procedure IncrementCounter()
begin
GlobalCounter += 1;
end;
begin
GlobalCounter := 1;
IncrementCounter();
Message('Counter is now: %1', GlobalCounter);
end;
Constants and Literals
You can use literal values directly in your code:
- Integer literals:
42,-7,0 - Decimal literals:
3.14,-0.5,100.00 - Text literals:
'Hello World','It''s a test'(use two single quotes for an apostrophe).
A text literal cannot span lines and has no backslash escapes.
- Boolean literals:
true,false - Date literals:
20240101D,0D(or useDMY2Date()) - Time literals:
120000T,120000.500T,0T - DateTime literal: only
0DT. Build other values withCreateDateTime(Date, Time). - Char literals: a one-character text such as
'A', or the character code65, wherever a
Char is expected
Identifiers, keywords, and member names are case-insensitive. Names that contain spaces or punctuation, or that collide with a keyword, are written in double quotes: "Sales Header", "No.", "Report". The reserved words are and or xor not div mod break else false true for foreach to downto procedure trigger if then exit var while do repeat until begin end case of page codeunit report run runmodal action; in and temporary are recognized by position and are not reserved.
Expressions and Operators
ToolBox supports the standard AL operators for building expressions.
Arithmetic Operators
| Operator | Description | Example |
|---|---|---|
+ |
Addition | A + B |
- |
Subtraction | A - B |
* |
Multiplication | A * B |
/ |
Division (decimal) | A / B |
div |
Integer division | A div B |
mod |
Modulo (remainder) | A mod B |
Comparison Operators
| Operator | Description | Example |
|---|---|---|
= |
Equal to | A = B |
<> |
Not equal to | A <> B |
< |
Less than | A < B |
> |
Greater than | A > B |
<= |
Less than or equal | A <= B |
>= |
Greater than or equal | A >= B |
Logical Operators
| Operator | Description | Example |
|---|---|---|
and |
Logical AND | A and B |
or |
Logical OR | A or B |
not |
Logical NOT | not A |
xor |
Exclusive OR | A xor B |
in |
Set membership | A in [1, 2, 3] |
Operator Precedence
ToolBox follows the standard AL precedence rules, which differ from C-style languages: and and xor bind as tightly as *, or binds like +, and the comparison operators bind loosest. Always put parentheses around each comparison when you combine them:
if (Amount > 1000) and (Customer.Blocked = Customer.Blocked::" ") then // correct
if Amount > 1000 and Customer.Blocked = Customer.Blocked::" " then // wrong: parsed as Amount > (1000 and ...)
| Level (high to low) | Operators |
|---|---|
| Unary | -x, +x, not x (not applies only to the primary expression that follows; write not (X in [1, 2])) |
| Multiplicative | *, /, div, mod, and, xor |
| Additive | +, -, or |
| Comparison | =, <>, <, <=, >, >=, in |
and and or do not short-circuit: both operands are always evaluated. Use a nested if when the right-hand side must only run if the left-hand side is true.
Ternary Operator
ToolBox adds a conditional expression that standard AL does not have:
Label := (Amount > 1000) ? 'Large' : 'Small';
Compound Assignment
+=, -=, *=, and /= work on variables, on record fields (Customer."Credit Limit (LCY)" += 100), and on array elements (Totals[i] += Amount).
String Concatenation
You use the + operator to concatenate text values:
var
FirstName: Text;
LastName: Text;
FullName: Text;
begin
FirstName := 'John';
LastName := 'Smith';
FullName := FirstName + ' ' + LastName;
Message(FullName);
end;
String Indexing
A Text variable can be indexed like an array of characters. S[i] reads the character at 1-based position i (as a Char; positions past the end read as 0), and S[i] := 'x' replaces that character. Writing to position StrLen(S) + 1 appends one character.
var
S: Text;
C: Char;
begin
S := 'hello';
C := S[1]; // 'h'
S[1] := 'H'; // S = 'Hello'
S[6] := '!'; // S = 'Hello!'
end;
Comments
ToolBox supports single-line comments using // and block comments using /* ... */:
var
Total: Decimal;
begin
// Calculate the total with tax
Total := 100.00;
Total := Total * 1.25; // Add 25% VAT
/* Multi-line comments are
also supported */
Message('Total with tax: %1', Total);
end;
Procedures
Procedures allow you to organize your code into reusable blocks. You can define parameters, return values, and local variables.
Declaring a Procedure
procedure CalculateDiscount(Amount: Decimal; DiscountPct: Decimal): Decimal
begin
exit(Amount * DiscountPct / 100);
end;
Procedure with Local Variables
procedure FormatCustomerInfo(CustomerNo: Code[20]): Text
var
Customer: Record Customer;
Result: Text;
begin
if Customer.Get(CustomerNo) then begin
Result := Customer."No." + ' - ' + Customer.Name;
if Customer.City <> '' then
Result := Result + ' (' + Customer.City + ')';
exit(Result);
end;
exit('Customer not found');
end;
Parameters and Return Types
- Parameters are passed by value unless marked
var. Records, JSON values, lists, dictionaries,
streams, and codeunits are reference types, so changes made through a parameter are visible to the caller even without var. Arrays are copied unless passed as var.
- Parameter types can be any declarable type, including
Record <Table>, `Recordtemporary
,Codeunit,List of [T],Dictionary of [K, V],array[N] of T, andOption A,B,C. Sized types (Text[50],Code[20]`) are accepted.- A procedure can return any scalar type,
Option, the four JSON types,List, orDictionary.
It cannot return a
Record, aCodeunit, or an array; pass those out through avarparameter.- A procedure with a return type that ends without
exit(...)returns the default value of its
return type.
- Procedures may call each other in any order and may be recursive.
Calling Procedures
Procedures are defined between the
varblock and the mainbegin/endblock. The main body calls them as needed. Parameterless calls may omit the parentheses (Customer.Reset;), but writing them is recommended.var OriginalPrice: Decimal; FinalPrice: Decimal; Info: Text; procedure CalculateDiscount(Amount: Decimal; DiscountPct: Decimal): Decimal begin exit(Amount * DiscountPct / 100); end; procedure FormatCustomerInfo(CustomerNo: Code[20]): Text var Customer: Record Customer; begin if Customer.Get(CustomerNo) then exit(Customer."No." + ' - ' + Customer.Name); exit('Customer not found'); end; begin OriginalPrice := 250.00; FinalPrice := OriginalPrice - CalculateDiscount(OriginalPrice, 10); Message('Final price after 10%% discount: %1', FinalPrice); Info := FormatCustomerInfo('10000'); Message(Info); end;Working with Records
Records are your primary way to interact with Business Central data. You declare a record variable by specifying the table it references.
Declaring Record Variables
var Customer: Record Customer; SalesHeader: Record "Sales Header"; Item: Record Item; begin // Access records through these variables end;Add the
temporarykeyword after the table name to get an in-memory record whose rows never touch the database — useful for buffering, sorting, or filtering data in memory:var TempCustomer: Record Customer temporary;See Temporary Records for the full behavior.
Accessing Fields
You access record fields using the dot notation. Field names with special characters must be enclosed in double quotes.
var Customer: Record Customer; begin if Customer.Get('10000') then begin Message('No.: %1', Customer."No."); Message('Name: %1', Customer.Name); Message('City: %1', Customer.City); Message('Balance: %1', Customer."Balance (LCY)"); end; end;Simple Record Iteration
var Customer: Record Customer; CustomerCount: Integer; begin Customer.SetRange("Country/Region Code", 'US'); if Customer.FindSet() then repeat CustomerCount += 1; Message('Customer: %1 - %2', Customer."No.", Customer.Name); until Customer.Next() = 0; Message('Total US customers: %1', CustomerCount); end;Variable Scope
Understanding variable scope is important for writing correct scripts.
- Script variables are declared in the script's
varblock beforebegin. They persist
for the entire script execution and are accessible from all procedures.
- Procedure variables are declared inside a procedure's
varblock. They are created
when the procedure starts and destroyed when it ends.
If a procedure declares a local variable with the same name as a script variable, the local variable takes precedence within that procedure.
var Status: Text; procedure ShowLocalStatus() var Status: Text; begin // This is a local Status variable, separate from the script-level one Message(Status); // Shows '' (empty, because local was never assigned) end; begin Status := 'Script value'; Message(Status); // Shows 'Script value' ShowLocalStatus(); end;Summary
The AL language in ToolBox gives you a powerful runtime-interpreted scripting environment that follows the familiar syntax and conventions of standard Business Central AL. You can declare variables, write procedures, interact with records, and build complete automation scripts without deploying an app or opening VS Code.
For more advanced topics, see the related documentation below.
Related Topics
- Control Flow Statements
- Types and Automatic Conversion
- Built-in Functions
- Arrays
- Collections (List and Dictionary)
- Json Support
- Codeunit Support
- Record Methods
- Limitations and Differences from Standard AL
Send Feedback
Current page: Loading... - A procedure can return any scalar type,