
10 AL Tips Every Business Central Developer Should Know
Ten practical AL coding tips to write cleaner, faster, and more maintainable Microsoft Dynamics 365 Business Central extensions.
Writing AL for Microsoft Dynamics 365 Business Central is as much about habits as it is about syntax. Small choices—how you check for records, load fields, or pass parameters—add up across extensions, upgrades, and team handoffs.
These ten tips are practical patterns you can apply today. They focus on code quality, maintainability, and performance without adding unnecessary complexity.
Whether you are shipping your first app or refining a mature codebase, keeping these practices in mind will help you write AL that is easier to read, safer to change, and faster at runtime.
Tip 001 – Never Hardcode Configuration Values in AL Code
Never hardcode configuration values in AL code.
❌ Bad: Hardcoded values ✅ Good: Store them in a Setup table
❌ Bad
HttpClient.Get('https://api.contoso.com/orders', Response);✅ Good
HttpClient.Get(MySetup."API Base URL", Response);Avoid hardcoding:
- 🌐 API URLs
- 🔑 API keys
- 📧 Email addresses
- 🔒 Passwords or secrets
Why?
- ✅ Easier to maintain.
- ✅ More secure.
- ✅ No code changes required when values change.
- ✅ Different environments can have different configurations.
Rule of thumb: If a value might change, don't hardcode it—put it in a Setup table.
Tip 002 – When Checking if Records Exist, Use IsEmpty
When checking if records exist, use IsEmpty because it's faster & better for performance!
| Approach | SQL behavior | Performance |
|---|---|---|
❌ Rec.Count > 0 | SELECT COUNT(*) (Counts all rows 😨) | Slower |
✅ NOT Rec.IsEmpty | SELECT TOP 1 (Stops at first match ⚡) | Faster |
Need to check if records exist? Use IsEmpty().
✅ Good: IsEmpty()
❌ Bad: Count() > 0
❌ Bad
if SalesLine.Count() > 0 then
Message('Sales lines exist.');✅ Good
if not SalesLine.IsEmpty() then
Message('Sales lines exist.');Tip 003 – Don't Call CalcFields Inside a Loop
Don't call CalcFields inside a loop.
✅ Use CalcFields() when you need FlowFields for one record. ✅ Use SetAutoCalcFields() before FindSet() when processing multiple records.
✅ Good: SetAutoCalcFields()
❌ Bad: CalcFields() on every iteration
❌ Bad
if Item.FindSet() then
repeat
Item.CalcFields(Inventory);
if Item.Inventory < Item."Reorder Point" then
NotifyReorder(Item);
until Item.Next() = 0;✅ Good
Item.SetAutoCalcFields(Inventory);
if Item.FindSet() then
repeat
if Item.Inventory < Item."Reorder Point" then
NotifyReorder(Item);
until Item.Next() = 0;- ✅ Reduces unnecessary database calls.
Tip 004 – Format Dates with FORMAT Instead of Building Strings Manually
Format dates with FORMAT instead of building strings manually.
✅ Best: Use FORMAT
❌ Avoid: Concatenating day, month, and year yourself
Why?
- ✅ Cleaner and easier to read.
- ✅ Less code and fewer opportunities for bugs.
- ✅ Supports formatting options directly.
- ✅ Easier to maintain if the format changes.
✅ Best
procedure DisplayFormattedDate()
var
MyDate: Date;
FormattedDate: Text;
begin
MyDate := Today();
FormattedDate := Format(MyDate, 0, '<Day,2>/<Month,2>/<Year4>');
Message('%1', FormattedDate);
end;❌ Avoid
procedure DisplayFormattedDate()
var
MyDate: Date;
FormattedDate: Text;
begin
MyDate := Today();
FormattedDate :=
Format(Date2DMY(MyDate, 1)) + '/' +
Format(Date2DMY(MyDate, 2)) + '/' +
Format(Date2DMY(MyDate, 3));
Message('%1', FormattedDate);
end;💡 Why this matters
Imagine you later need to display:
2026-07-06
or
06 Jul 2026
With FORMAT, you simply change the format string:
Format(MyDate, 0, '<Year4>-<Month,2>-<Day,2>');No need to rewrite the entire concatenation logic.
Tip 005 – Use TextBuilder Instead of String Concatenation
Use TextBuilder instead of string concatenation:
❌ Bad Practice: String Concatenation ✅ Good Practice: TextBuilder
Tested with 200K loops results speak for themselves!
//✅ 54 ms
procedure AppendWithTextBuilder200k()
var
TempText: TextBuilder;
i: Integer;
begin
for i := 1 to 200000 do
TempText.Append('x');
end;//❌ 9 sec 494 ms
procedure AppendWithString200k()
var
TempText: Text;
i: Integer;
begin
for i := 1 to 200000 do
TempText += 'x';
end;Tip 006 – Convert Simple Types to Text with ToText
Convert Simple Types to Text with ToText:
Old way (Before 2025 wave 1 (BC26) 👎)
MyText := FORMAT(MyDate);New way (After 2025 wave 1 (BC26) 👍)
MyText := MyDate.ToText();✅ More intuitive syntax ✅ Cleaner & more readable code
✅ Directly available on simple types (BigInteger, Boolean, Byte, Date, DateTime, Decimal, Duration, Guid, Integer, Label, Time, Version) for easy conversion to text.
Keep using FORMAT for advanced formatting, but for quick conversions, switch to ToText!
Tip 007 – Use Overloaded GetValue Methods to Retrieve JSON Values Directly
use overloaded GetValue methods to retrieve JSON values directly:
❌ Bad Practice:
if JsonObj.Get('MyNumber', JsonToken) then
MyInteger := JsonToken.AsInteger();✅ Good Practice:
MyInteger := JsonObj.GetInteger('MyNumber');Tip 008 – Speed Up Record Retrieval by Only Loading Needed Fields
Speed up record retrieval by only loading needed fields:
Use Rec.SetLoadFields for Optimized Performance!
✅ Improves performance ✅ Reduces database load ✅ Loads only what's needed!
Before (Slow! 👎)
if Rec.FindSet() then
repeat
Message('%1', Rec.Description);
until Rec.Next() = 0;Now (Optimized! 👍)
Rec.SetLoadFields(Description);
if Rec.FindSet() then
repeat
Message('%1', Rec.Description);
until Rec.Next() = 0;Tip 009 – Understand var Parameters to Control Data Flow in Procedures
Understand var parameters to control data flow in procedures:
Passing parameters with var allows the procedure to modify the caller's value without var, only a copy is passed.
❌ Without var
procedure UpdateCustomerName(Customer: Record Customer)
begin
Customer.Name := 'New Name';
end;
UpdateCustomerName(Cust);
// Cust.Name is NOT changed✅ With var
procedure UpdateCustomerName(var Customer: Record Customer)
begin
Customer.Name := 'New Name';
end;
UpdateCustomerName(Cust);
// Cust.Name is changedRule:
➡️ No var = read-only input ➡️ With var = modify the caller value
✅ Clear intent (input vs output) ✅ Avoid hidden side effects ✅ Write safer, cleaner AL code
Tip 010 – Prefer Enums over Options for Table Fields
Prefer Enums over Options for table fields
❌ Bad – Option in a table field
field(10; Status; Option)
{
OptionMembers = Open,"In Progress",Closed;
}
UpdateStatus(1); // What does 1 mean?✅ Better – Use an Enum
field(10; Status; Enum "Document Status")
{
}
UpdateStatus("Document Status"::"In Progress");✅ Type-safe no magic numbers. ✅ Self-documenting code. ✅ Extensible with enumextension.
Conclusion
These ten tips are small on their own, but together they shape AL that is easier to maintain, safer to extend, and faster in production. Pick one or two to apply in your current project, then layer in the rest as you review and refactor. 🚀 Consistent habits beat one-off optimizations—and your future self (and your team) will thank you for it.
Discussion
Sign in to join the discussion.