In this video, I show how you can create a custom mask for a field, either for protecting information or for formatting. Check it out:

In this video, Erik demonstrates how to create custom field masking in Business Central using only AL code. While BC offers built-in masking options (like password fields with asterisks), sometimes you need something more tailored — such as displaying only the last four digits of a credit card number. Erik walks through building this from scratch, explaining the key design decisions along the way.
The Use Case: Masking a Credit Card Number
The example scenario is storing a credit card number on the Customer Card, but only showing the last four digits to users. When a user enters the number, the actual value gets saved to the database, but what’s displayed on screen is a masked version like *************4321.
Important disclaimer: Erik emphasizes that you should never actually store credit card numbers in Business Central. This is purely used as an illustrative example for the masking technique. If you need to store sensitive data, look into proper secrets management.
Why You Can’t Just Use a Function
One of the key design challenges Erik highlights is that for a page field to be editable, its source expression must be something assignable — either a table field or a global variable. You can’t put a procedure call as the source expression of a field and still allow users to type into it. If you only needed to display a value, a function would work fine, but since users need to enter the credit card number, we need a variable.
This is why the solution uses a global variable (MaskedCreditCard) as the field’s source expression, rather than binding directly to the table field or a function.
The Solution Architecture
The approach has three main parts:
- A table extension to store the actual credit card number
- A global variable on the page to hold the masked (displayed) value
- A masking function that converts the real value into its masked representation
Building the Table Extension
First, we extend the Customer table with a new field to hold the raw credit card number:
tableextension 50100 MyCustomer extends Customer
{
fields
{
field(50100; CreditCardNo; Text[50])
{
Caption = 'Credit Card No.';
}
}
}
This field stores the unmasked, full credit card number. It’s a straightforward Text[50] field on the Customer table.
Building the Page Extension
The page extension is where the magic happens. Let’s break it down piece by piece:
pageextension 50100 MyCustomer extends "Customer Card"
{
layout
{
addafter(Name)
{
field(CreditCardCtl; MaskedCreditCard)
{
Caption = 'Credit Card No.';
ApplicationArea = all;
trigger OnValidate()
begin
if strlen(MaskedCreditCard) < 5 then
error('Not a valid credit card');
Rec.CreditCardNo := MaskedCreditCard;
MaskedCreditCard := PerformMasking();
end;
}
}
}
Notice that the field control is bound to MaskedCreditCard (the global variable), not to Rec.CreditCardNo (the table field). This is the crucial trick. When the user types a value and tabs out, the OnValidate trigger fires, which:
- Validates that the entered value is at least 5 characters (basic sanity check)
- Saves the raw entered value into the actual table field (
Rec.CreditCardNo) - Immediately replaces the displayed value with the masked version
Populating the Mask on Page Load
When the user opens the Customer Card or navigates between records, we need the masked value to be populated. This is handled with two triggers:
trigger OnAfterGetRecord()
begin
MaskedCreditCard := PerformMasking();
end;
trigger OnAfterGetCurrRecord()
begin
MaskedCreditCard := PerformMasking()
end;
Erik notes that using both OnAfterGetRecord and OnAfterGetCurrRecord might seem redundant, but it's a "better safe than sorry" approach. Depending on the page type and how records are fetched, one or the other may fire. Since the masking operation is very lightweight, calling it twice has no meaningful performance impact.
The Masking Function
procedure PerformMasking(): Text
begin
if Strlen(Rec.CreditCardNo) > 4 then
exit('*************' + Rec.CreditCardNo.Substring(strlen(Rec.CreditCardNo) - 3))
else
exit('');
end;
var
MaskedCreditCard: Text[50];
}
The PerformMasking function checks whether the stored credit card number has more than four characters. If so, it returns a string of asterisks concatenated with the last four characters. The Substring call uses StrLen - 3 because Substring is 1-based — if the string is 10 characters long, starting at position 7 gives you characters 7, 8, 9, and 10 (the last four). If the value is four characters or fewer, it returns an empty string.
Testing It Out
Erik demonstrates the solution in action:
- Opening the Customer Card shows a blank credit card field
- Typing a number ending in
4321and tabbing out displays*************4321 - Navigating to another customer shows a blank field (no credit card entered)
- Entering a different number ending in
7890and navigating back confirms each customer retains their own masked value - Copying the displayed value only copies the asterisks and last four digits — there's no data leak from the UI
Security Considerations
Erik points out one caveat: a power user with access to Page Inspection can still see the actual CreditCardNo field value in the underlying table data. For truly sensitive data, you'd want to combine this masking technique with other security measures, such as using the isolated storage or Azure Key Vault for secrets management.
Beyond Credit Cards: Other Uses
The PerformMasking function is where all the creativity lives. Erik suggests several other applications for this same pattern:
- Phone number formatting: Users type raw digits, but the display adds parentheses, spaces, and dashes (e.g.,
(555) 123-4567) - Social security / tax ID masking: Show only the last few digits
- Custom formatting: Any scenario where you want to store raw data but display it differently
The technique isn't limited to hiding data — it works equally well for formatting displayed values while keeping the stored data clean and unformatted.
Summary
The key takeaway is a simple but effective pattern for custom field masking in AL:
- Store the real value in a table field that's not directly exposed on the page
- Use a global variable as the page field's source expression so it remains editable
- On
OnValidate, transfer the entered value to the real field, then immediately replace the variable with the masked version - On
OnAfterGetRecordandOnAfterGetCurrRecord, populate the variable with the masked version
This gives you full control over how data is displayed to users while preserving the original value in the database — all with pure AL, no external tools needed.