8.1 Codeunit Types, Execution & SingleInstance Codeunits
Key Takeaways
- Codeunits encapsulate procedural business logic, complex algorithms, and transaction processing in AL without presenting a native visual user interface.
- Setting TableNo on a codeunit binds it to a specific table context, passing the record implicitly as Rec into OnRun and enabling conditional execution via if Codeunit.Run(...) then.
- Parameter passing distinguishes value evaluation (stack copies) from reference evaluation (var), where reference passing avoids expensive record buffer duplication on the Navision Server Tier.
- Codeunit Subtypes (Normal, Install, Upgrade, Test, TestRunner) establish the object execution lifecycle, trigger availability, and transaction boundary behaviors.
- SingleInstance codeunits persist their global variables and state in memory for the lifespan of a user session, serving as high-performance session caches and cross-event state accumulators.
8.1 Codeunit Types, Execution & SingleInstance Codeunits
In Microsoft Dynamics 365 Business Central, codeunits serve as the foundational container for procedural business logic, data processing algorithms, posting routines, and integration workflows. Unlike tables and pages, codeunits have no direct visual user interface (UI) representations; instead, they encapsulate reusable procedures, execute database transactions, manage state, and respond to platform events. For developers preparing for the MB-820 certification exam, mastering codeunit anatomy, parameter passing semantics, subtype specializations, and the memory architecture of SingleInstance codeunits is vital for Domain 3.
1. Codeunit Anatomy & Execution Mechanics
A codeunit object in AL is declared using the codeunit keyword followed by an integer ID and a unique quoted name. It contains an optional OnRun trigger, global and local procedure declarations, event publishers/subscribers, and variable definitions.
codeunit 50100 "Sales Processing Manager"
{
TableNo = "Sales Header";
Access = Public;
Subtype = Normal;
trigger OnRun()
begin
// Rec refers to the Sales Header record passed into Codeunit.Run(SalesHeaderRec)
ProcessSalesDocument(Rec);
end;
var
GlobalPostingDate: Date;
AuditCounter: Integer;
procedure ProcessSalesDocument(var SalesHeader: Record "Sales Header"): Boolean
var
SalesLine: Record "Sales Line";
LineCount: Integer;
begin
SalesHeader.TestField("Document Type");
SalesHeader.TestField("No.");
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
SalesLine.SetRange("Document No.", SalesHeader."No.");
if SalesLine.FindSet() then begin
LineCount := SalesLine.Count();
ValidatePostingConditions(SalesHeader);
exit(true);
end;
exit(false);
end;
local procedure ValidatePostingConditions(var SalesHeader: Record "Sales Header")
begin
// Private helper procedure restricted to this codeunit
if SalesHeader."Posting Date" = 0D then
SalesHeader."Posting Date" := WorkDate();
end;
}
The TableNo Property and OnRun Trigger
TableNo: When specified,TableNobinds the codeunit to a specific database table. Inside theOnRuntrigger, the system implicitly declaresRec(andxRec) as record variables pointing to that table.- Invocation with Context: Calling
Codeunit.Run(Codeunit::"Sales Processing Manager", SalesHeaderRec)passes the record reference directly toRec. If the codeunit modifiesRecand completes successfully, changes are reflected in the caller's record buffer. - Boolean Return (
if Codeunit.Run(...) then): Codeunit execution can be wrapped in anIFstatement. When executed conditionally, runtime errors inside the codeunit are caught silently, returningfalsewithout terminating the parent transaction. This pattern allows developers to trap unhandled runtime errors, inspectGetLastErrorText(), and perform custom rollback or logging logic.
Codeunit Run vs. [TryFunction]
While [TryFunction] also catches runtime errors and returns a Boolean, it has critical platform limitations compared to conditional codeunit execution:
- A
[TryFunction]cannot execute any database write operations (Insert,Modify,Delete) or commit transactions (Commit) if an error is caught, because doing so leaves the database in an inconsistent state. - In contrast, running a codeunit conditionally via
if Codeunit.Run(...) thenexecutes the entire codeunit inside an isolated transaction block. If an error occurs inside the codeunit, all database modifications made within that codeunit execution are rolled back automatically, while allowing the caller to continue execution cleanly.
2. Variable Scoping & Parameter Evaluation Semantics
Understanding how AL manages memory, allocates stack/heap space, and evaluates procedure parameters is crucial for both writing performant code and passing the MB-820 exam.
Variable Scopes in AL
- Global Variables: Declared in the
varsection at the root level of the codeunit. Global variables are accessible by all procedures within the codeunit. For standard codeunits (SingleInstance = false), global variables are reinitialized each time the codeunit is invoked. ForSingleInstance = truecodeunits, global variables retain their values across multiple procedure calls for the entire duration of the client session. - Local Variables: Declared within the
varblock of an individual procedure. They are allocated on the execution stack when the procedure is entered and disposed of immediately upon procedure exit. Local variables cannot be accessed from outside the declaring procedure.
Parameter Passing: Value vs. Reference (var)
| Mechanism | AL Syntax | Memory Behavior | Modification Impact |
|---|---|---|---|
| Pass by Value | procedure Calc(Amount: Decimal) | Allocates a new variable on the stack and copies the caller's value. | Modifications inside the procedure affect only the local copy; the caller's variable remains unchanged. |
Pass by Reference (var) | procedure Calc(var Amount: Decimal) | Passes a direct memory reference (pointer) to the caller's variable. | Any modification inside the procedure directly mutates the caller's source variable. |
| Record Pass by Value | procedure Process(Cust: Record Customer) | Clones the entire record buffer and filter state in NST memory. | Inefficient for large loops; changes to fields or filters do not alter the caller's record buffer. |
| Record Pass by Reference | procedure Process(var Cust: Record Customer) | Passes a pointer to the existing record buffer in memory. | Highly efficient (zero cloning overhead); modifications and filter changes apply directly to the caller's record. |
Exam Watchout — Record Buffer Overhead: Passing large
Recordvariables or temporary record buffers by value forces the Navision Server Tier (NST) to clone the entire table buffer and active filter groups in memory. Always pass records with thevarkeyword unless you explicitly require an isolated, disposable copy of the record state.
Procedure Access Modifiers
AL supports granular access scoping on procedures to enforce encapsulation across modular extensions:
procedure(Public): Default scope. Accessible by any object within the current extension and any external extension that references this extension as a dependency.internal procedure: Accessible only by objects within the same extension package, or by friend extensions explicitly declared inapp.jsonvia theinternalsVisibleToproperty.local procedure: Private to the declaring codeunit. Cannot be called by any other object, page, or codeunit.protected procedure: Accessible within the declaring object and any object that extends it (used primarily in table and page extension scenarios).
3. Codeunit Subtypes Deep Dive
The Subtype property classifies a codeunit for dedicated platform roles, enabling specialized triggers and transactional behavior.
+-----------------------------------------------------------------------------------------+
| CODEUNIT SUBTYPES IN AL |
+---------------+-------------------------------------------------------------------------+
| Normal | Default execution model. Standard procedures, triggers, and events. |
+---------------+-------------------------------------------------------------------------+
| Install | Runs during extension installation/sync per tenant and per company. |
+---------------+-------------------------------------------------------------------------+
| Upgrade | Executes structured multi-phase data migrations across extension builds.|
+---------------+-------------------------------------------------------------------------+
| Test | Houses automated test methods decorated with [Test] for Test Toolkit. |
+---------------+-------------------------------------------------------------------------+
| TestRunner | Manages test isolation, test suite execution, and telemetry logging. |
+---------------+-------------------------------------------------------------------------+
Subtype Breakdown & Characteristics
-
Subtype = Normal(Default):- Standard procedural and object-oriented business logic.
- Contains general procedures, event publishers, event subscribers, and posting logic.
- Instantiated on demand; global state is destroyed once the calling scope exits (unless
SingleInstance = true).
-
Subtype = Install:- Houses the extension setup lifecycle triggers:
OnInstallAppPerCompany()andOnInstallAppPerDatabase(). - Used to populate setup records, insert default seed data, configure number series, and register background tasks when an extension is deployed or reinstalled.
- Cannot be run manually from the UI or via
Codeunit.Run().
- Houses the extension setup lifecycle triggers:
-
Subtype = Upgrade:- Houses the six-step upgrade pipeline triggers (
OnCheckPreconditions...,OnUpgrade...,OnValidateUpgrade...). - Used to transform, migrate, and validate data when upgrading an extension to a higher version.
- Only codeunit subtype permitted to instantiate and execute
DataTransferbulk SQL operations.
- Houses the six-step upgrade pipeline triggers (
-
Subtype = Test:- Designed for automated testing via the Business Central Test Toolkit.
- Procedures decorated with the
[Test]attribute execute business logic and verify assertions usingAssert.AreEqual(). - Can include UI handler methods decorated with
[MessageHandler],[ConfirmHandler],[PageHandler],[ModalPageHandler],[StrMenuHandler], and[ReportHandler]to simulate user interactions non-interactively.
-
Subtype = TestRunner:- Orchestrates execution across multiple
Testcodeunits (Subtype = Test). - Contains triggers:
OnBeforeTestRun()andOnAfterTestRun(). - Controls the
TestIsolationproperty:Disabled: No rollback. Database modifications made by tests persist in the database.Codeunit: Database changes made by all test methods within a Test codeunit are rolled back together when the codeunit finishes.Function: Database changes made by each individual test procedure are rolled back immediately upon procedure completion, ensuring pristine test isolation.
- Orchestrates execution across multiple
4. SingleInstance = true Architecture & Patterns
By default, codeunits are ephemeral: the NST creates a new instance in memory when a procedure is called and disposes of the instance when execution concludes. When a codeunit is configured with SingleInstance = true, the NST alters its memory allocation model fundamentally.
codeunit 50105 "Session State Cache"
{
SingleInstance = true;
var
UserPreferenceCache: Dictionary of [Code[20], Text];
AuditCounter: Integer;
TempLogBuffer: Record "Activity Log" temporary;
procedure SetUserPreference(KeyName: Code[20]; PreferenceValue: Text)
begin
if UserPreferenceCache.ContainsKey(KeyName) then
UserPreferenceCache.Set(KeyName, PreferenceValue)
else
UserPreferenceCache.Add(KeyName, PreferenceValue);
end;
procedure GetUserPreference(KeyName: Code[20]; var PreferenceValue: Text): Boolean
begin
exit(UserPreferenceCache.Get(KeyName, PreferenceValue));
end;
procedure LogSessionEvent(Context: Text[100]; Description: Text[250])
begin
AuditCounter += 1;
TempLogBuffer.Init();
TempLogBuffer."Entry No." := AuditCounter;
TempLogBuffer."Context" := Context;
TempLogBuffer."Description" := Description;
TempLogBuffer.Insert();
end;
procedure GetLogEntries(var TargetBuffer: Record "Activity Log" temporary)
begin
TargetBuffer.Copy(TempLogBuffer, true);
end;
procedure ClearCache()
begin
Clear(UserPreferenceCache);
TempLogBuffer.Reset();
TempLogBuffer.DeleteAll();
AuditCounter := 0;
end;
}
Architectural Characteristics of SingleInstance Codeunits
- Session-Scoped Persistence: Only one instance of the codeunit exists per client session. All pages, reports, and codeunits executing within that same session access the identical instance and share its global variables.
- In-Memory Caching: Avoids redundant database round-trips for static or semi-static configuration parameters across complex user interactions.
- Event Accumulator Pattern: Allows event subscribers located inside the
SingleInstancecodeunit to accumulate telemetry, transactional history, or validation flags across completely disparate table and page events throughout a session. - Session Isolation & Concurrency: SingleInstance codeunits are never shared across different users, web client browser sessions, or distinct background sessions. Each session instantiates its own isolated copy. When a background task (
TaskScheduler.CreateTask()) or API request executes, it runs in a separate session with its own independent instance. - Memory Management Considerations: Global collections (e.g.,
List of [T],Dictionary of [K, V], temporary tables) stored in aSingleInstancecodeunit remain in server RAM until the user logs out or the session times out. Developers must implement explicitClear()routines to prevent session memory bloat.
A developer needs to accumulate audit log entries across multiple distinct page actions and table triggers during a single user's interactive session, without writing intermediate records to the physical database until the final posting action. Which codeunit design should be implemented?
When passing a large Record variable to a global procedure in AL, what is the primary architectural benefit of specifying the 'var' keyword in the procedure parameter signature?
Which codeunit Subtype is specifically designed to manage test execution isolation, orchestrate test suites, and handle automatic rollback of database changes made by automated tests?
A developer executes Codeunit.Run wrapped inside an IF statement: 'if not Codeunit.Run(Codeunit::"Post Batch", Rec) then HandleError();'. What occurs at runtime if a runtime error is raised inside the codeunit?