Control Flow Statements
Introduction
ToolBox supports all the standard AL control flow statements you are familiar with from Business Central development. These statements allow you to make decisions, repeat operations, and control the execution path of your scripts.
This page covers every control flow construct available in ToolBox, with syntax details, examples, and common pitfalls for each.
if-then-else
The if statement executes code conditionally based on a Boolean expression.
Syntax
if <condition> then
<statement>;
if <condition> then
<statement>
else
<statement>;
Description
When the condition evaluates to true, the statement after then executes. If an else clause is present and the condition is false, the statement after else executes instead.
Examples
Simple if statement:
var
Amount: Decimal;
begin
Amount := 1500.00;
if Amount > 1000 then
Message('Large order detected: %1', Amount);
end;
if-then-else with compound statements:
var
Customer: Record Customer;
begin
if Customer.Get('10000') then begin
Message('Found customer: %1', Customer.Name);
Message('City: %1', Customer.City);
end else begin
Message('Customer not found');
Error('Cannot proceed without a valid customer');
end;
end;
Nested if statements:
var
Amount: Decimal;
CustomerType: Text;
Discount: Decimal;
begin
Amount := 500.00;
CustomerType := 'VIP';
if Amount > 100 then begin
if CustomerType = 'VIP' then
Discount := 20
else
Discount := 10;
end else
Discount := 0;
Message('Discount: %1%%', Discount);
end;
Common Pitfalls
- Forgetting
begin/endwhen you need multiple statements in a branch. Without them,
only the first statement belongs to the if or else.
- Placing a semicolon before
else. The semicolon terminates theifstatement, so the
else becomes orphaned and causes a compilation error.
- Writing combined conditions without parentheses.
andbinds tighter than>in AL, so
if A > 1 and B > 2 then is parsed as A > (1 and B) > 2. Always write if (A > 1) and (B > 2) then.
- Relying on short-circuit evaluation.
andandorevaluate both operands, so
if Customer.Get(No) and (Customer.Name <> '') then still reads Name when Get fails. Use a nested if instead.
- Deeply nested
ifstatements become difficult to read. Consider using acasestatement
instead when you have many branches.
case
The case statement selects one of several branches based on the value of an expression.
Syntax
case <expression> of
<value1>:
<statement>;
<value2>, <value3>:
<statement>;
else
<statement>;
end;
Description
ToolBox evaluates the expression and compares it against each label. When a match is found, the corresponding statement executes. If no match is found and an else clause is present, the else statement executes.
A single branch can list several labels separated by commas, and a label can be a range written with ... You can mix single values and ranges freely:
case Score of
0:
Message('Zero');
1, 3, 5, 7, 9:
Message('Odd single digit');
2 .. 8:
Message('Even, 2 to 8');
10 .. 100:
Message('Two or three digits');
else
Message('Something else');
end;
Examples
Basic case statement:
var
DayOfWeek: Integer;
begin
DayOfWeek := Date2DWY(Today(), 1);
case DayOfWeek of
1:
Message('Monday');
2:
Message('Tuesday');
3:
Message('Wednesday');
4:
Message('Thursday');
5:
Message('Friday');
6, 7:
Message('Weekend');
end;
end;
Case with compound statements and else:
var
SalesHeader: Record "Sales Header";
StatusText: Text;
begin
if SalesHeader.FindFirst() then begin
case SalesHeader.Status of
SalesHeader.Status::Open:
begin
StatusText := 'Open';
Message('Order is open and can be edited');
end;
SalesHeader.Status::Released:
begin
StatusText := 'Released';
Message('Order is released for processing');
end;
SalesHeader.Status::"Pending Approval":
begin
StatusText := 'Pending Approval';
Message('Order is awaiting approval');
end;
else begin
StatusText := 'Unknown';
Message('Unrecognized order status');
end;
end;
end;
end;
Case for handling option values:
procedure GetPriorityLabel(Priority: Integer): Text
begin
case Priority of
1:
exit('Critical');
2:
exit('High');
3:
exit('Medium');
4, 5:
exit('Low');
else
exit('Undefined');
end;
end;
Common Pitfalls
- Forgetting the closing
end;for thecaseblock. - Not including an
elseclause to handle unexpected values. Always consider adding one
for safety.
- When using compound statements inside a case branch, you must wrap them in
begin/end.
The else branch is the exception: it accepts several statements up to the closing end without a begin/end block.
- Labels are compared with the same rules as
=: aCodevalue matches its label
case-insensitively, and a Decimal 1.0 matches the label 1.
for-to and for-downto
The for loop repeats a statement a fixed number of times, counting either up or down.
Syntax
for <variable> := <start> to <end> do
<statement>;
for <variable> := <start> downto <end> do
<statement>;
Description
The for-to loop increments the counter variable by 1 on each iteration, starting at <start> and ending when it exceeds <end>. The for-downto loop decrements by 1, ending when the counter falls below <end>.
Examples
Simple counting loop:
var
I: Integer;
begin
for I := 1 to 10 do
Message('Iteration: %1', I);
end;
Counting down:
var
I: Integer;
begin
Message('Countdown:');
for I := 5 downto 1 do
Message('%1...', I);
Message('Go!');
end;
Building a text value in a loop:
var
I: Integer;
Result: Text;
begin
for I := 1 to 5 do begin
if I > 1 then
Result := Result + ', ';
Result := Result + Format(I * 10);
end;
Message('Values: %1', Result);
// Output: Values: 10, 20, 30, 40, 50
end;
Common Pitfalls
- The loop counter must be an
IntegerorBigIntegervariable; other types are rejected. - Modifying the loop counter variable inside the loop body. This can lead to unexpected
behavior and should be avoided.
- Using
forwhen the number of iterations is not known at the start. Usewhileor
repeat instead.
- Forgetting
begin/endwhen the loop body has multiple statements.
while-do
The while loop repeats a statement as long as a condition remains true. The condition is evaluated before each iteration.
Syntax
while <condition> do
<statement>;
Description
Before each iteration, ToolBox evaluates the condition. If it is true, the statement executes. If the condition is false from the start, the statement never executes.
Examples
Basic while loop:
var
Counter: Integer;
begin
Counter := 1;
while Counter <= 5 do begin
Message('Counter: %1', Counter);
Counter += 1;
end;
end;
Processing until a condition is met:
var
Remaining: Decimal;
PaymentAmount: Decimal;
PaymentCount: Integer;
begin
Remaining := 1000.00;
PaymentAmount := 150.00;
while Remaining > 0 do begin
PaymentCount += 1;
if Remaining < PaymentAmount then
PaymentAmount := Remaining;
Remaining := Remaining - PaymentAmount;
Message('Payment %1: %2 (Remaining: %3)', PaymentCount, PaymentAmount, Remaining);
end;
end;
Conditional record processing with while:
var
Item: Record Item;
LowStockCount: Integer;
begin
Item.SetFilter(Inventory, '<%1', 10);
if Item.FindSet() then begin
while (LowStockCount < 5) and (Item.Next() <> 0) do begin
LowStockCount += 1;
Message('Low stock: %1 - %2 (Qty: %3)', Item."No.", Item.Description, Item.Inventory);
end;
end;
end;
Common Pitfalls
- Creating infinite loops by forgetting to update the condition variable inside the loop.
Always ensure the loop condition will eventually become false.
- Using
whilewhenrepeat-untilwould be more natural, especially for record iteration
with FindSet/Next.
repeat-until
The repeat-until loop executes a block of statements and then checks a condition. The loop continues until the condition becomes true.
Syntax
repeat
<statements>;
until <condition>;
Description
The statements between repeat and until execute at least once. After each iteration, ToolBox evaluates the condition. If it is true, the loop ends. If false, the loop repeats. Note that repeat-until does not require begin/end for multiple statements.
Examples
The classic FindSet/Next pattern:
This is the most common pattern in Business Central AL and ToolBox. You use it to iterate through a set of records.
var
Customer: Record Customer;
TotalBalance: Decimal;
begin
Customer.SetRange("Country/Region Code", 'US');
if Customer.FindSet() then
repeat
Customer.CalcFields("Balance (LCY)");
TotalBalance += Customer."Balance (LCY)";
Message('Customer %1: Balance = %2', Customer.Name, Customer."Balance (LCY)");
until Customer.Next() = 0;
Message('Total balance for US customers: %1', TotalBalance);
end;
Repeat with a counter:
var
Attempt: Integer;
Success: Boolean;
procedure TryProcess(AttemptNo: Integer): Boolean
begin
Message('Processing attempt %1...', AttemptNo);
exit(AttemptNo >= 2);
end;
begin
repeat
Attempt += 1;
Success := TryProcess(Attempt);
if not Success then
Sleep(1000);
until Success or (Attempt >= 3);
if Success then
Message('Succeeded on attempt %1', Attempt)
else
Error('Failed after %1 attempts', Attempt);
end;
Processing all lines of a sales order:
var
SalesLine: Record "Sales Line";
OrderTotal: Decimal;
begin
SalesLine.SetRange("Document Type", SalesLine."Document Type"::Order);
SalesLine.SetRange("Document No.", 'SO-001000');
if SalesLine.FindSet() then
repeat
OrderTotal += SalesLine."Line Amount";
Message('Line %1: %2 x %3 = %4',
SalesLine."Line No.",
SalesLine.Description,
SalesLine.Quantity,
SalesLine."Line Amount");
until SalesLine.Next() = 0;
Message('Order total: %1', OrderTotal);
end;
Common Pitfalls
- Forgetting that
repeat-untilalways executes at least once. If you need to skip the
loop entirely when the condition is already met, use while-do instead.
- In the
FindSet/Nextpattern, forgetting theif FindSet() thencheck before the
repeat. Without it, you would process an empty record if no records match.
- Using
Customer.Next() = 0as the until condition is correct becauseNext()returns 0
when there are no more records.
begin-end (Compound Statements)
The begin/end block groups multiple statements into a single compound statement.
Syntax
begin
<statement1>;
<statement2>;
...
end;
Description
Anywhere a single statement is expected in AL, you can use a begin/end block to execute multiple statements. This is essential for if, for, while, and case constructs when the body consists of more than one statement.
Example
var
Customer: Record Customer;
begin
if Customer.Get('10000') then begin
Message('No.: %1', Customer."No.");
Message('Name: %1', Customer.Name);
Message('Phone: %1', Customer."Phone No.");
Message('Email: %1', Customer."E-Mail");
end;
end;
Common Pitfalls
- Mismatched
begin/endpairs. Eachbeginmust have a correspondingend. - Placing a semicolon after
endwhen it is followed byelse. Theendbeforeelse
must not have a semicolon.
exit
The exit statement immediately leaves the current procedure or the main body.
Syntax
exit;
exit(<expression>);
Description
When called without a parameter, exit simply returns from the current procedure. When called with a value, it returns that value as the procedure's result. In the main body, exit ends the script execution.
Examples
Exiting a procedure early:
procedure ProcessCustomer(CustomerNo: Code[20]): Boolean
var
Customer: Record Customer;
begin
if CustomerNo = '' then
exit(false);
if not Customer.Get(CustomerNo) then
exit(false);
if Customer.Blocked <> Customer.Blocked::" " then
exit(false);
// Process the customer
Message('Processing customer: %1', Customer.Name);
exit(true);
end;
Returning a calculated value:
procedure GetDiscountPercent(Amount: Decimal): Decimal
begin
if Amount >= 10000 then
exit(15);
if Amount >= 5000 then
exit(10);
if Amount >= 1000 then
exit(5);
exit(0);
end;
Common Pitfalls
- Using
exitwith a value in a procedure that does not declare a return type. This causes
a compilation error.
- Forgetting that
exitin the main body ends the entire script execution.
break
The break statement exits the innermost loop immediately.
Syntax
break;
Description
When you call break inside a for, while, or repeat loop, execution jumps to the first statement after the loop. If you have nested loops, break only exits the innermost one.
Examples
Breaking out of a for loop:
var
Customer: Record Customer;
I: Integer;
begin
if Customer.FindSet() then
for I := 1 to 100 do begin
Message('Customer: %1', Customer.Name);
if Customer.Next() = 0 then
break;
end;
end;
Breaking from a repeat loop when a condition is met:
var
Item: Record Item;
HighValueItem: Text;
begin
Item.SetCurrentKey("Unit Price");
Item.Ascending(false);
if Item.FindSet() then
repeat
if Item."Unit Price" < 100 then
break;
Message('High value item: %1 (%2)', Item.Description, Item."Unit Price");
until Item.Next() = 0;
end;
Common Pitfalls
- Using
breakoutside of a loop. This causes a compilation error. - Expecting
breakto exit multiple nested loops. It only exits the innermost loop.
in (Set Membership)
The in operator tests whether a value is a member of a set literal. The set is written with square brackets and can contain single values and ranges (..), just like case labels. The result is a Boolean, so in is typically used in an if or while condition.
Syntax
if <value> in [<value1>, <value2> .. <value3>, ...] then
<statement>;
Examples
var
Priority: Integer;
Grade: Char;
begin
Priority := 4;
if Priority in [1, 3 .. 5] then
Message('High or medium priority');
// Combine with not
Grade := 'B';
if not (Grade in ['A' .. 'C']) then
Message('Below passing');
end;
Using in is clearer than a chain of or comparisons:
// These two conditions are equivalent:
if (Status = 1) or (Status = 3) or (Status = 4) or (Status = 5) then ...
if Status in [1, 3 .. 5] then ...
The
inkeyword is also used by theforeachloop (below). ToolBox tells the two uses apart by context, soinis not otherwise a reserved identifier.
foreach
The foreach statement iterates over the elements of a List, a JsonArray, or any method result that returns a list (such as Text.Split(), Dictionary.Keys(), or Regex.Split()). It is the collection counterpart to repeat/for.
Syntax
foreach <element> in <collection> do
<statement>;
Description
The loop variable takes each element of the collection in turn. Its type must match the collection's element type. For a JsonArray, the loop variable must be a JsonToken.
Not for dictionaries or arrays. Writing
foreach X in MyDictionary dois a runtime error; iterateMyDictionary.Keys()orMyDictionary.Values()instead. Iterating a fixed-size array withforeachis a compile error (matching native AL); use an indexedforloop.
Examples
Iterating a list:
var
Csv: Text;
Colors: List of [Text];
Color: Text;
begin
Csv := 'red,green,blue';
Colors := Csv.Split(','); // Split returns a List of [Text]
foreach Color in Colors do
Message('Color: %1', Color);
end;
Iterating a JSON array:
var
Arr: JsonArray;
Tok: JsonToken;
begin
Arr.ReadFrom('[1, 2, 3]');
foreach Tok in Arr do
Message('%1', Tok.AsValue().AsInteger());
end;
Iterating dictionary keys:
var
Stock: Dictionary of [Text, Integer];
ItemNo: Text;
begin
Stock.Add('1000', 25);
Stock.Add('1100', 40);
foreach ItemNo in Stock.Keys() do
Message('Item %1: %2 in stock', ItemNo, Stock.Get(ItemNo));
end;
See Collections for the full List and Dictionary APIs.
Running Objects: Codeunit.Run, Page.Run, Report.Run
Three statements run a Business Central object by its object number, optionally passing a record variable. They are statements, not expressions, so they cannot appear inside a larger expression and have no return value.
Syntax
Codeunit.Run(<id>[, <record variable>]);
Page.Run(<id>[, <record variable>]);
Page.RunModal(<id>[, <record variable>]);
Report.Run(<id>[, <record variable>]);
Report.RunModal(<id>[, <record variable>]);
The object id is an integer expression (object names and Codeunit::Name constants are not accepted). The record argument must be the name of a record variable.
Examples
var
Customer: Record Customer;
begin
Customer.SetRange("Country/Region Code", 'US');
Page.Run(22, Customer); // Customer List with the filter applied
Customer.Get('10000');
Page.RunModal(21, Customer); // Customer Card, waits until closed
Report.RunModal(1306); // Standard Sales - Invoice request page
Codeunit.Run(50100, Customer); // Run any codeunit by number
end;
Page.Run, Page.RunModal, and Report.RunModal need a user session and cannot be used in Job Queue runs. Only the supported codeunits can be called member by member through a codeunit variable; every other codeunit is limited to Codeunit.Run.
Practical Examples
Iterating Through Records with Conditional Logic
var
Customer: Record Customer;
ActiveCount: Integer;
BlockedCount: Integer;
begin
if Customer.FindSet() then
repeat
if Customer.Blocked = Customer.Blocked::" " then
ActiveCount += 1
else
BlockedCount += 1;
until Customer.Next() = 0;
Message('Active: %1, Blocked: %2', ActiveCount, BlockedCount);
end;
Nested Loops for Master-Detail Processing
var
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
OrderTotal: Decimal;
begin
SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order);
if SalesHeader.FindSet() then
repeat
OrderTotal := 0;
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
SalesLine.SetRange("Document No.", SalesHeader."No.");
if SalesLine.FindSet() then
repeat
OrderTotal += SalesLine."Line Amount";
until SalesLine.Next() = 0;
Message('Order %1: Total = %2', SalesHeader."No.", OrderTotal);
until SalesHeader.Next() = 0;
end;
Using Case for Complex Routing
var
Customer: Record Customer;
begin
if Customer.FindSet() then
repeat
case true of
Customer."Balance (LCY)" > 100000:
AssignToSeniorRep(Customer);
Customer."Balance (LCY)" > 10000:
AssignToRep(Customer);
Customer."No. of Orders" = 0:
AssignToNewCustomerTeam(Customer);
else
AssignToGeneralPool(Customer);
end;
until Customer.Next() = 0;
end;