If you’ve built anything in NAV or Business Central, you’ve used a setup table. The problem is—most legacy patterns break the moment you leave the UI.
This guide shows the correct modern AL pattern for singleton setup tables, including concurrency handling, naming conventions, and reusable design.
What Is a Singleton Setup Table?
In Microsoft Dynamics 365 Business Central, a singleton setup table is a table designed to hold exactly one record — your extension’s configuration. Think of General Ledger Setup, Sales & Receivables Setup, or Inventory Setup. One row. Always there. Always accessible.
When you build a custom extension, you’ll almost always need one of these. Maybe you need to store a default dimension, a posting group override, or a number series code your extension uses. That’s your singleton setup table.
The Classic C/AL Pattern (NAV Days)
If you’ve been around since NAV 2013–2017, this pattern lives rent-free in your head:
OnOpenPage()
RESET;
IF NOT GET THEN BEGIN
INIT;
INSERT;
END;Simple. Effective. Gets the job done. The page opens, the record is created if it doesn’t exist, and life goes on.
But here’s the problem — the logic lives entirely in the page. The moment any other object (a report, a codeunit, a job queue) needs that same setup record, you either duplicate the logic or pray the user opened the setup page first.
In real projects, that prayer fails. A client calls saying a report errored out — and the root cause is a missing setup record because the setup page was never opened on that environment.
The Modern AL Pattern
Modern AL encourages you to move this responsibility into the table itself as a reusable procedure. Here’s a realistic example — a setup table for a custom extension that needs a default No. Series and a Global Dimension Code override.
The Setup Table
namespace Abinash.ExtensionName;
table 50100 "ABD My Extension Setup"
{
Caption = 'My Extension Setup';
DataClassification = CustomerContent;
// DataPerCompany = false; // Uncomment only if config should be shared across all companies
fields
{
field(1; "Primary Key"; Code[10])
{
Caption = 'Primary Key';
}
field(10; "ABD No. Series"; Code[20])
{
Caption = 'No. Series';
ToolTip = 'Specifies the number series used to assign numbers in this extension.';
TableRelation = "No. Series";
}
field(20; "ABD Default Global Dimension 1"; Code[20])
{
Caption = 'Default Global Dimension 1';
ToolTip = 'Specifies the default global dimension 1 code applied on extension transactions.';
TableRelation = "Dimension Value".Code
where("Global Dimension No." = const(1));
}
}
keys
{
key(PK; "Primary Key") { Clustered = true; }
}
procedure GetSetup()
begin
if not Get('') then begin
Init();
"Primary Key" := '';
// Handle concurrency — two sessions may try to insert at the same time
if not Insert(true) then
Get('');
end;
end;
}PRO TIP:
Insert(true)ensures OnInsert triggers fire — important if another extension later extends this table.
WARNING: Without the concurrency-safe pattern above, background sessions like job queues or APIs can intermittently fail in high-load environments when two sessions hit
GetSetup()at the exact same time.
The Setup Page
namespace Abinash.ExtensionName;
page 50100 "ABD My Extension Setup"
{
Caption = 'My Extension Setup';
PageType = Card;
SourceTable = "ABD My Extension Setup";
UsageCategory = Administration;
ApplicationArea = All;
DeleteAllowed = false;
InsertAllowed = false;
layout
{
area(Content)
{
group(General)
{
Caption = 'General';
field("ABD No. Series"; Rec."ABD No. Series")
{
ApplicationArea = All;
ToolTip = 'Specifies the number series used by this extension.';
}
field("ABD Default Global Dimension 1"; Rec."ABD Default Global Dimension 1")
{
ApplicationArea = All;
ToolTip = 'Specifies the default global dimension 1 code.';
}
}
}
}
trigger OnOpenPage()
begin
Rec.GetSetup(); // Rec. prefix is correct here — this is a page trigger, not a table procedure
end;
}Calling It From a Report or Codeunit
This is where the pattern pays off. Any object in your extension reads setup the same clean way:
namespace Abinash.ExtensionName;
codeunit 50100 "ABD Some Process"
{
var
MyExtSetup: Record "ABD My Extension Setup";
NoSeriesCode: Code[20];
DimCode: Code[20];
procedure RunProcess()
begin
MyExtSetup.GetSetup();
// Now safely use setup values anywhere
NoSeriesCode := MyExtSetup."ABD No. Series";
DimCode := MyExtSetup."ABD Default Global Dimension 1";
end;
}No duplicate GET logic scattered across reports, codeunits, and job queues. One procedure. Called from everywhere.
Three Common Questions
1. Blank key ” or named key like ‘SETUP’?
Use blank ''. Every Microsoft setup table does it this way — General Ledger Setup, Sales & Receivables Setup, all of them. Using 'SETUP' works but it’s non-standard and adds zero value.
Here’s what both look like so you can spot the difference in the wild:
// ❌ Non-standard — works but unnecessary
procedure GetSetup()
begin
if Rec.Get('SETUP') then
exit;
Rec.Init();
Rec."Primary Key" := 'SETUP';
Rec.Insert(true);
end;
// ✅ Microsoft standard — clean, concurrency-safe, recognized instantly
procedure GetSetup()
begin
if not Get('') then begin
Init();
"Primary Key" := '';
if not Insert(true) then
Get('');
end;
end;Notice also that inside a table’s own procedure, you don’t need the Rec. prefix — Get(), Init(), and Insert() implicitly refer to the table itself. Rec. is page-level syntax.
2. Should I use a prefix/suffix on field names?
Short answer — yes, if you’re targeting AppSource. Microsoft requires a registered affix (exactly 3 characters) on all custom object and field names to avoid collisions with other extensions. For internal extensions, a namespace alone is sufficient, but using an affix is a good habit regardless.
// Internal only — namespace is enough
namespace Abinash.ExtensionName;
field(10; "No. Series"; Code[20]) { }
// AppSource / multi-extension safe — namespace + affix
namespace Abinash.ExtensionName;
field(10; "ABD No. Series"; Code[20]) { }To register your affix, email [email protected] with your MPN ID, publisher name, and at least five 3-character suggestions. See Microsoft’s prefix and suffix requirements for full details. Pick something short and unique to your company.
3. Why gap-based field numbering (10, 20, 30)?
field(1; "Primary Key"; Code[10]) { } // Always 1
field(10; "ABD No. Series"; Code[20]) { } // First real field
field(20; "ABD Default Global Dim 1"; Code[20]) { } // Room to insert between
field(30; "ABD Something Added Later"; Boolean) { }Field gaps let you insert new fields between existing ones later without breaking logical grouping. It’s a small habit that saves real pain on growing extensions.
The Key Takeaway
| Aspect | Old C/AL Pattern | Modern AL Pattern |
|---|---|---|
| Where logic lives | Page OnOpenPage | Table GetSetup procedure |
| Reusable by reports / codeunits | ❌ | ✅ |
| Works with job queues / APIs | ❌ | ✅ |
| Concurrency safe | ❌ | ✅ |
| Setup guaranteed | Only if page is opened | Anywhere it’s called |
| Microsoft standard key | ” | ” |
| Namespace support | ❌ | ✅ |
The old pattern isn’t wrong — it’s just incomplete for modern BC development where reports, APIs, and job queues all need setup data independently of the UI.
Put the logic in the table. Call it from everywhere. One source of truth.
Technical FAQ
Q: Should I always call GetSetup() before using the record?
Yes. Never assume the record exists — especially in job queues or API calls that run outside the UI.
Q: Can I cache the setup record for performance?
Start without caching — it’s rarely needed. If you’re calling setup hundreds of times per transaction, consider a SingleInstance codeunit as a thin cache layer.
Q: Should the setup table be DataPerCompany?
Usually yes — that’s the default. Only set DataPerCompany = false if you intentionally want the config shared across all companies in the environment.
Q: Why not just insert the record in the OnInstall codeunit?
You can and should — but you still need runtime safety. OnInstall won’t cover every edge case (environment restores, database migrations, etc.).
Q: Singleton setup table vs SingleInstance codeunit — what’s the difference?
Completely different concepts. A singleton setup table means one record in the database. A SingleInstance codeunit means one instance in memory per session. Don’t mix them up.
Built this pattern differently? Found an edge case I missed? Let’s hear it below.