Select Page
Business Central Codeunit: Complete Properties

March 31, 2026

Part 1: Codeunit in Business Central

A codeunit is an AL object that contains business logic, procedures, and functions. It has no visual interface and is used purely to hold and organize code that can be called from other objects like pages, reports, or other codeunits.

Codeunits are the standard place to write reusable logic such as calculations, validations, integrations, and workflow processing.

data permission inserts valued testing logic, procedures or function need to step by step explain

Basic Codeunit Structure

codeunit 50100 "My First Codeunit"
{
    trigger OnRun()
    begin
        Message('Hello from codeunit!');
    end;
    procedure DoSomething()
    begin
        // your logic here
    end;
}

Codeunit Properties

1. Subtype

Defines the role of the codeunit in the platform.

  • Normal – General purpose codeunit for business logic. This is the default.
  • Test – Used for automated unit testing.
  • TestRunner – Runs and orchestrates multiple Test codeunits.
  • Upgrade – Runs data migration logic when the extension is upgraded.
  • Install – Runs setup logic when the extension is installed for the first time.
codeunit 50101 "My Upgrade"
{
    Subtype = Upgrade;
    trigger OnUpgradePerCompany()
    begin
        // migration logic
    end;
}

2. TableNo

Binds the codeunit to a table. When set, the Rec variable in the OnRun trigger is automatically of that table type.

codeunit 50102 "Process Item"
{
    TableNo = Item;
    trigger OnRun()
    begin
        // Rec is an Item record
        Rec.Validate(Blocked, false);
        Rec.Modify(true);
    end;
}

3. SingleInstance

When set to true, only one instance of the codeunit exists per session. All callers share the same object and its global variables. Useful for session-level caching.

codeunit 50103 "Session Cache"
{
    SingleInstance = true;
    var
        CachedName: Text;
}

4. Permissions

Declares which tables this codeunit can access. R = Read, I = Insert, M = Modify, D = Delete.

codeunit 50104 "Data Handler"
{
    Permissions =
        tabledata Customer = R,
        tabledata "Sales Header" = RIMD;
}

5. Access

Controls whether the codeunit is visible outside its extension.

  • Public – Visible and callable from any extension. This is the default.
  • Internal – Only usable within the same extension.

6. EventSubscriberInstance

Controls how the codeunit is created when an event subscriber fires.

  • StaticAutomatic – No instance is created. Best for stateless subscribers. This is the default.
  • Manual – You manually bind the instance. Use when subscribers need to share state.

7. InherentPermissions

Grants the codeunit inherent runtime permissions regardless of the calling user’s permissions. Commonly set to X for execute.

8. InherentEntitlements

Sets the minimum licence entitlement needed to run the codeunit.

9. ObsoleteState, ObsoleteReason, ObsoleteTag

Used to deprecate a codeunit over time.

  • ObsoleteState – No (active), Pending (warning), or Removed (compile error).
  • ObsoleteReason – Text explaining why it is deprecated and what to use instead.
  • ObsoleteTag – Version string when the deprecation was introduced.
codeunit 50105 "Old Handler"
{
    ObsoleteState = Pending;
    ObsoleteReason = 'Use codeunit 50200 New Handler instead.';
    ObsoleteTag = '23.0';
}

10. Procedure Access Modifiers

Procedures inside a codeunit can have different visibility levels.

codeunit 50106 "Access Demo"
{
    // Callable from anywhere
    procedure PublicMethod()
    begin
    end;
    // Only callable within this codeunit
    local procedure LocalMethod()
    begin
    end;
    // Only callable within the same extension
    internal procedure InternalMethod()
    begin
    end;
}

11. Error Handling

Codeunits use Error() or ErrorInfo to stop execution and report problems to the user.

procedure ValidateAmount(Amount: Decimal)
var
    ErrInfo: ErrorInfo;
begin
    if Amount <= 0 then begin
        ErrInfo.Title := 'Invalid Amount';
        ErrInfo.Message := 'Amount must be greater than zero.';
        Error(ErrInfo);
    end;
end;

Part 2: Codeunit Extension in Business Central

A codeunit extension allows you to add behaviour to an existing base application or third-party codeunit without modifying its source code. This follows the extension model that Business Central uses throughout AL development.

Codeunit extensions are primarily used to subscribe to events published by the base codeunit, or to add new procedures that complement the base codeunit’s functionality.

Basic Codeunit Extension Structure

codeunitextension 50200 "My Sales Post Extension"
    extends "Sales-Post"
{
    // Add event subscribers or new procedures here
}

Subscribing to Events in a Codeunit Extension

The most common use of a codeunit extension is to subscribe to events raised by the base codeunit. This lets you inject custom logic at specific points in the base process.

codeunitextension 50201 "Sales Post Custom Logic"
    extends "Sales-Post"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post",
        'OnBeforePostSalesDoc', '', false, false)]
    local procedure BeforePostSalesDoc(var SalesHeader: Record "Sales Header")
    begin
        if SalesHeader.Amount <= 0 then
            Error('Cannot post a sales order with zero amount.');
    end;
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post",
        'OnAfterPostSalesDoc', '', false, false)]
    local procedure AfterPostSalesDoc(var SalesHeader: Record "Sales Header")
    begin
        // custom logic after posting completes
    end;
}

Adding New Procedures in a Codeunit Extension

You can also add brand new procedures to extend the functionality associated with the base codeunit. These procedures are only available when your extension is installed.

codeunitextension 50202 "Item Processing Extension"
    extends "Item-Check Avail."
{
    procedure LogAvailabilityCheck(ItemNo: Code[20]; Qty: Decimal)
    begin
        // custom logging logic
        Message('Availability checked for item %1, qty %2.', ItemNo, Qty);
    end;
}

Codeunit Extension Properties

1. Access

Same as on a regular codeunit. Controls whether the extension object itself is visible to other extensions.

2. EventSubscriberInstance

Controls how the extension is instantiated when event subscribers fire. Works the same way as on a regular codeunit.

  • StaticAutomatic – No instance created. Best for stateless event logic.
  • Manual – You control when the instance is bound and unbound.

3. ObsoleteState, ObsoleteReason, ObsoleteTag

Used to mark a codeunit extension as deprecated, following the same pattern as on regular codeunits.

Important Rules for Codeunit Extensions

  1. You cannot override or replace existing procedures in the base codeunit. You can only add new procedures or subscribe to events.
  2. You cannot change any properties of the base codeunit from within the extension.
  3. Event subscribers in a codeunit extension work identically to those in a standalone codeunit.
  4. The extends keyword must reference the exact name or ID of the base codeunit.

Practical Example: Extending the Posting Codeunit

codeunitextension 50203 "Custom Post Validation"
    extends "Sales-Post"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post",
        'OnBeforePostSalesDoc', '', false, false)]
    local procedure ValidateCustomFields(var SalesHeader: Record "Sales Header")
    var
        SalesHeaderExt: Record "Sales Header";
    begin
        SalesHeaderExt.Get(SalesHeader."Document Type", SalesHeader."No.");
        // validate any custom fields added via table extension
    end;
}

Part 3: Codeunit Fields — Variables, Parameters & Return Values

Codeunits do not have fields in the same way that tables do. Instead, they use variables to hold data during execution. This section covers all the ways data is declared, passed, and returned inside a codeunit.

Global vs Local Variables

Variables in a codeunit can be declared at two different scopes.

Global variables are declared at the codeunit level outside any procedure. They exist for the entire lifetime of the codeunit instance and are shared across all procedures in that instance.

Local variables are declared inside a specific procedure. They exist only while that procedure is running and are destroyed when the procedure ends.

codeunit 50300 "Variable Demo"
{
    // Global - shared across all procedures
var
        TotalAmount: Decimal;
        IsProcessed: Boolean;
    procedure Calculate()
    var
        // Local - only exists inside this procedure
        LineAmount: Decimal;
        Counter: Integer;
    begin
        LineAmount := 100;
        TotalAmount += LineAmount;
    end;
}

Scalar Data Types

The following are the most common data types used for variables in codeunits.

  • Integer – Whole numbers from about negative 2 billion to positive 2 billion. Use for counts and loop counters.
  • BigInteger – 64-bit whole number for very large values.
  • Decimal – Fixed-point decimal number with up to 18 significant digits. Always use for monetary amounts.
  • Text – Variable-length string with no fixed size limit.
  • Text[n] – Fixed-length string where n is the maximum character count, for example Text[50].
  • Code[n] – Like Text but automatically converts to uppercase. Used for identifiers and keys.
  • Boolean – Holds true or false.
  • Date – A calendar date without time.
  • Time – A time of day without a date.
  • DateTime – A combined date and time value.
  • GUID – A globally unique 128-bit identifier.
  • Blob – Stores binary data such as images or file content.
var
    OrderNo: Code[20];
    CustomerName: Text[100];
    InvoiceAmount: Decimal;
    PostingDate: Date;
    IsPosted: Boolean;
    RecordId: GUID;

Record Variables

A record variable represents a row or set of rows from a database table. Record variables are one of the most frequently used types in codeunits.

procedure
GetCustomerBalance(CustNo: Code[20]) Balance: Decimal
var
    Customer: Record Customer;
    LedgerEntry: Record "Cust. Ledger Entry";
begin
    Customer.Get(CustNo);
    LedgerEntry.SetRange("Customer No.", CustNo);
    LedgerEntry.CalcSums(Amount);
    Balance := LedgerEntry.Amount;
end;

Passing Parameters

Parameters are variables passed into a procedure when it is called. By default they are passed by value, meaning a copy is made and the original is not changed.

// Passed by VALUE - original is not changed
procedure AddTax(Amount: Decimal)
begin
    Amount := Amount * 1.2; // only affects the local copy
end;

Adding the var keyword before a parameter passes it by reference. This means the procedure works directly on the original variable and any changes are reflected back to the caller.

// Passed by REFERENCE - original is updated
procedure AddTaxRef(var Amount: Decimal)
begin
    Amount := Amount * 1.2; // updates the caller's variable
end;

Always pass Record variables with var unless you specifically want to work on a copy. Passing large records by value is inefficient.

Return Values

A procedure can return a single value by declaring a return variable after the parameter list.

// Returns a Decimal
procedure CalcDiscount(BaseAmt: Decimal; Pct: Decimal) DiscountAmt: Decimal
begin
    DiscountAmt := BaseAmt * (Pct / 100);
end;
// Returns a Record
procedure GetCustomer(CustNo: Code[20]) Cust: Record Customer
begin
    Cust.Get(CustNo);
end;

Temporary Record Variables

A temporary record variable is an in-memory table that does not write to the database. It is useful for staging data, building intermediate result sets, or passing complex data between procedures without persisting it.

procedure BuildTempLines(var TempLine: Record "Sales Line" temporary)
var
    SalesLine: Record "Sales Line";
begin
    if SalesLine.FindSet() then
        repeat
            TempLine.Init();
            TempLine."Line No." := SalesLine."Line No.";
            TempLine.Quantity := SalesLine.Quantity;
            TempLine.Insert();
        until SalesLine.Next() = 0;
end;

Enum Variables

Enums are a modern replacement for integer option fields. They give your code type safety and make the intent of values clear.

enum 50000 "Order Status"
{
    value(0; Open) { Caption = 'Open'; }
    value(1; Released) { Caption = 'Released'; }
    value(2; Closed) { Caption = 'Closed'; }
}
procedure SetStatus(var Header: Record "Sales Header"; Status: Enum "Order Status")
begin
    Header.Status := Status;
    Header.Modify(true);
end;

Interface Variables

Interfaces allow you to write code against an abstraction rather than a concrete codeunit. This makes it easier to swap implementations without changing the calling code.

interface IPaymentProcessor
{
    procedure ProcessPayment(Amount: Decimal): Boolean;
}
procedure RunPayment(Processor: Interface IPaymentProcessor; Amount: Decimal)
begin
    if not Processor.ProcessPayment(Amount) then
        Error('Payment failed.');
end;

Summary of Variable Concepts

  • Global variables – Declared at codeunit level, shared across all procedures in the instance.
  • Local variables – Declared inside a procedure, destroyed when the procedure ends.
  • Pass by value – A copy is passed. The original is not affected.
  • Pass by reference (var) – The original variable is passed. Changes affect the caller.
  • Return values – A procedure can return one named value declared after the parameter list.
  • Temporary records – In-memory table instances that do not persist to the database.
  • Enums – Type-safe named values used instead of raw integers.
  • Interfaces – Abstractions that allow flexible, swappable implementations.