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.
Last updated: August 2026

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, TableNo binds the codeunit to a specific database table. Inside the OnRun trigger, the system implicitly declares Rec (and xRec) as record variables pointing to that table.
  • Invocation with Context: Calling Codeunit.Run(Codeunit::"Sales Processing Manager", SalesHeaderRec) passes the record reference directly to Rec. If the codeunit modifies Rec and completes successfully, changes are reflected in the caller's record buffer.
  • Boolean Return (if Codeunit.Run(...) then): Codeunit execution can be wrapped in an IF statement. When executed conditionally, runtime errors inside the codeunit are caught silently, returning false without terminating the parent transaction. This pattern allows developers to trap unhandled runtime errors, inspect GetLastErrorText(), 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:

  1. 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.
  2. In contrast, running a codeunit conditionally via if Codeunit.Run(...) then executes 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.
Loading diagram...
Standard vs. SingleInstance Codeunit Lifecycle in NST Memory

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

  1. Global Variables: Declared in the var section 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. For SingleInstance = true codeunits, global variables retain their values across multiple procedure calls for the entire duration of the client session.
  2. Local Variables: Declared within the var block 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)

MechanismAL SyntaxMemory BehaviorModification Impact
Pass by Valueprocedure 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 Valueprocedure 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 Referenceprocedure 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 Record variables 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 the var keyword 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 in app.json via the internalsVisibleTo property.
  • 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

  1. 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).
  2. Subtype = Install:

    • Houses the extension setup lifecycle triggers: OnInstallAppPerCompany() and OnInstallAppPerDatabase().
    • 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().
  3. 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 DataTransfer bulk SQL operations.
  4. Subtype = Test:

    • Designed for automated testing via the Business Central Test Toolkit.
    • Procedures decorated with the [Test] attribute execute business logic and verify assertions using Assert.AreEqual().
    • Can include UI handler methods decorated with [MessageHandler], [ConfirmHandler], [PageHandler], [ModalPageHandler], [StrMenuHandler], and [ReportHandler] to simulate user interactions non-interactively.
  5. Subtype = TestRunner:

    • Orchestrates execution across multiple Test codeunits (Subtype = Test).
    • Contains triggers: OnBeforeTestRun() and OnAfterTestRun().
    • Controls the TestIsolation property:
      • 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.

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 SingleInstance codeunit 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 a SingleInstance codeunit remain in server RAM until the user logs out or the session times out. Developers must implement explicit Clear() routines to prevent session memory bloat.
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D