Understanding the Record Data Type in Business Central, coming from other data frameworks, can be a challenge. It’s very simple but also complicated. In this video, I break the Record variable into different layers and describe each of them.

In this ELI5 (Explain It Like I’m Five) video, Erik breaks down one of the most commonly asked questions from developers coming to Business Central’s AL language from other frameworks: why is everything crammed into a single Record variable? He explains the three conceptual layers hidden inside every record variable — the context, the dataset, and the record pointer — and demonstrates each with practical code examples.
The Question: Why Just One Variable Type?
If you come from .NET, Java, or many other data frameworks, you’re used to a clear hierarchy of objects when working with data. You might have a database context, a dataset or query object, and then individual record or entity objects. In AL and Business Central, all of that functionality is collapsed into a single type: the Record variable.
This can seem strange at first, but once you understand the three layers that a Record variable encapsulates, it starts to make a lot more sense.
Declaring Record Variables
Let’s start with the basics. Here’s how you declare record variables in AL:
var
Customer: Record Customer;
Customer2: Record Customer temporary;
Customer3: Record Customer;
All three of these are record variables pointing at the Customer table, but they each serve different purposes. Customer is a standard record variable connected to the Customer table in the database. Customer2 is marked as temporary, meaning it works with an in-memory table that starts out blank and is completely separate from the database. Customer3 is another standard record variable that we’ll use to demonstrate company context switching.
The Three Layers of a Record Variable
Erik identifies three distinct layers of functionality all living inside that single Record variable:
- The Context — Where am I? Which company? Database or temporary?
- The Dataset — Filters, ranges, and aggregate operations like Count
- The Record (Pointer) — A pointer to one specific record and its field values
Let’s look at each one in detail.
Layer 1: The Context
The context determines where your record variable is looking for data. In Business Central, a single database can contain multiple companies, and each company effectively has its own copy of every table. The tables share the same schema and fields, but the data is separate.
You can switch which company a record variable points to using ChangeCompany:
// Switch to a different company's customer table
Customer3.ChangeCompany('My Company');
message('Count = %1', Customer3.Count());
// Switch back to the current company
Customer3.ChangeCompany(CompanyName());
message('Count = %1', Customer3.Count());
The temporary keyword is another context-level concept. When you declare a record variable as temporary, you’re telling the system to work with an in-memory table instead of the actual database table. It’s blank to start with, and anything you insert into it only exists for the lifetime of that variable.
In other frameworks, you’d typically define this context through a separate connection or context object. In AL, it’s baked right into the record variable itself. This also means that if you need to change company on ten different record variables, you need ten separate ChangeCompany calls — there’s no shared owning context.
Layer 2: The Dataset
The dataset layer is where you define which records you’re interested in. This is analogous to building a WHERE clause in SQL. AL provides two main methods for setting filters:
SetFilter allows you to use filter expressions with wildcards and pattern matching:
// Filter customers whose name starts with "Peter"
Customer.SetFilter(Name, 'Peter*');
// Filter addresses containing "9"
Customer.SetFilter(Address, '*9*');
SetRange is used for absolute values or ranges (translating to a SQL BETWEEN clause). If you pass a single value, it acts as an exact match filter:
// Filter to only customers blocked for invoicing
Customer.SetRange(Blocked, Customer.Blocked::Invoice);
You can also call aggregate functions on the dataset. For example, Count returns the number of records matching your current filters:
message('Count = %1', Customer.Count());
An important detail: Count does not move the record pointer. Whatever specific record was currently loaded in memory remains loaded after calling Count. These dataset operations are independent from the record pointer — they define the set of records you’re working with, not which individual record you’re looking at.
Layer 3: The Record (Pointer)
The record pointer is what most people think of first — it represents a single, specific record and its field values. You can read and write field values directly:
// Directly assign a value (not recommended)
Customer.Name := 'Erik';
However, Erik points out that directly assigning values to fields is actually a bad pattern. The better approach is to use Validate:
// Validate simulates user input and runs business logic
Customer.Validate(Name, 'Erik');
Validate simulates a user typing the value into the field on a page. It triggers all the associated business logic — field-level triggers, related field updates, and validation rules. This ensures that what you’re putting into the database is consistent and correct.
To move the pointer to an actual database record, you use functions like FindFirst:
Customer.FindFirst();
This goes to the database, applies whatever filters you’ve set at the dataset layer, and moves the pointer to the first matching record. After this call, the field values on your record variable (like Customer.Name) will reflect the data from that first matching record.
The Interplay Between Layers
Here’s where things get interesting — and potentially confusing. The dataset and the record pointer are separate but coexist on the same variable. Consider this scenario:
Customer.Name := 'Erik'; // Record pointer has Name = 'Erik'
Customer.SetFilter(Name, 'Peter*'); // Dataset filter: names starting with Peter
At this point, Customer.Name still contains ‘Erik’ — that’s what’s in the record buffer in memory. But the dataset filter is set to ‘Peter*’. The currently loaded record doesn’t even match the filter! They’re not independent (they share the same variable), but they are separate pieces of functionality operating at different levels.
The Complete Example
Here’s the full code example that demonstrates all three layers:
pageextension 50100 CustomerListExt extends "Customer List"
{
trigger OnOpenPage();
var
Customer: Record Customer;
Customer2: Record Customer temporary; // Context
Customer3: Record Customer;
begin
// The Record (Pointer to a specific record)
Customer.Name := 'Erik';
Customer.Validate(Name, 'Erik');
// The DataSet
Customer.SetFilter(Name, 'Peter*');
Customer.SetFilter(Address, '*9*');
Customer.SetRange(Blocked, Customer.Blocked::Invoice);
message('Count = %1', Customer.Count());
// Context
Customer3.ChangeCompany('My Company');
message('Count = %1', Customer3.Count());
Customer3.ChangeCompany(CompanyName());
message('Count = %1', Customer3.Count());
end;
}
Summary
In Business Central’s AL language, there is only one variable type for representing table data: the Record. But don’t let its simplicity fool you — it packs three conceptual layers into one:
- Context — Controls where the data comes from (which company, database vs. temporary). Key functions:
ChangeCompany, thetemporarykeyword. - Dataset — Defines which records you’re working with through filters and ranges. Key functions:
SetFilter,SetRange,Count,FindFirst,FindSet. - Record Pointer — Points to a single specific record and its field values. Key operations: reading/writing fields,
Validate,Insert,Modify,Delete.
If you’re coming from other frameworks where these responsibilities are split across multiple object types, understanding this three-layer model will help you work with AL record variables much more effectively.