Erik's ToolBox Documentation
Index

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/end when 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 the if statement, so the

else becomes orphaned and causes a compilation error.

  • Writing combined conditions without parentheses. and binds 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. and and or evaluate both operands, so

if Customer.Get(No) and (Customer.Name <> '') then still reads Name when Get fails. Use a nested if instead.

  • Deeply nested if statements become difficult to read. Consider using a case statement

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 the case block.
  • Not including an else clause 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 =: a Code value 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 Integer or BigInteger variable; other types are rejected.
  • Modifying the loop counter variable inside the loop body. This can lead to unexpected

behavior and should be avoided.

  • Using for when the number of iterations is not known at the start. Use while or

repeat instead.

  • Forgetting begin/end when 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 while when repeat-until would 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-until always 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/Next pattern, forgetting the if FindSet() then check before the

repeat. Without it, you would process an empty record if no records match.

  • Using Customer.Next() = 0 as the until condition is correct because Next() 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/end pairs. Each begin must have a corresponding end.
  • Placing a semicolon after end when it is followed by else. The end before else

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 exit with a value in a procedure that does not declare a return type. This causes

a compilation error.

  • Forgetting that exit in 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 break outside of a loop. This causes a compilation error.
  • Expecting break to 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 in keyword is also used by the foreach loop (below). ToolBox tells the two uses apart by context, so in is 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 do is a runtime error; iterate MyDictionary.Keys() or MyDictionary.Values() instead. Iterating a fixed-size array with foreach is a compile error (matching native AL); use an indexed for loop.

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;