Keep your numbers sane with number series

Another entry in my long list of videos for AL beginners. This time it’s all about numbers series.

https://youtu.be/jmsic5-V5WM

In this video, Erik walks through how to implement Number Series in AL for Business Central. He covers what Number Series are, how they work under the hood, the correct way to use them in your custom apps (hint: use the NoSeriesManagement codeunit), and several gotchas you should be aware of — including the “Allow Gaps” setting, delayed inserts, and unexpected UI pop-ups during transactions.

What Are Number Series?

Number Series is a concept that has been in Dynamics NAV/Business Central for ages. It provides automatic, sequential numbering for records — think customer numbers, invoice numbers, order numbers, and so on. Despite being a foundational feature, it can be surprisingly confusing to work with.

To find Number Series in Business Central, search for “Number Series” (note: the table name uses the abbreviation No. — as in No. Series — even though we’re talking about “numbers”). Within the Number Series construct, you have:

  • Number Series — the header record that identifies a particular series (e.g., “DUDE” for our example).
  • Number Series Lines — the lines that actually hold the starting number, ending number, last used number, and date ranges.

The “Allow Gaps” Setting

There’s an important setting on Number Series Lines called Allow Gaps in Nos. that is often misunderstood. Erik emphasizes that this is not a reverse setting — unchecking it does not guarantee gap-free numbering. Rather, enabling “Allow Gaps” is a performance enhancement. When turned on, Business Central does not table-lock the Number Series Lines record the same way, which prevents the number series from becoming a bottleneck in high-transactional environments. The trade-off is that gaps may appear in your numbering sequence.

Building a Sample App: The Dude List

Erik demonstrates with a simple custom app called “The Dude List” — a table with a number, name, and city, along with a list page and a card page. Initially, numbers are entered manually. The goal is to wire up automatic numbering via Number Series.

The Dude Table

table 54300 Dude
{
    Caption = 'Dude';
    DataClassification = ToBeClassified;

    fields
    {
        field(1; No; Code[20])
        {
            Caption = 'No';
            DataClassification = ToBeClassified;
        }
        field(2; Name; Text[100])
        {
            Caption = 'Name';
            DataClassification = ToBeClassified;
        }
        field(3; City; Text[50])
        {
            Caption = 'City';
            DataClassification = ToBeClassified;
        }
    }
    keys
    {
        key(PK; No)
        {
            Clustered = true;
        }
    }
    trigger OnInsert()
    var
        Setup: Record "Dude Setup";
        NoMgt: Codeunit NoSeriesManagement;
        Ref: RecordRef;
    begin
        Ref.Open(DATABASE::Dude);

        if No = '' then begin
            Setup.Get();
            No := NoMgt.GetNextNo(Setup."No. Series for Dude", WORKDATE, true);
        end;
    end;
}

The Dude List Page

page 54300 "Dude List"
{
    ApplicationArea = All;
    Caption = 'Dude List';
    PageType = List;
    SourceTable = Dude;
    UsageCategory = Lists;
    CardPageId = "Dude Card";

    layout
    {
        area(content)
        {
            repeater(General)
            {
                field(No; Rec.No)
                {
                    ApplicationArea = All;
                }
                field(Name; Rec.Name)
                {
                    ApplicationArea = All;
                }
                field(City; Rec.City)
                {
                    ApplicationArea = All;
                }
            }
        }
    }
}

The Dude Card Page

page 54301 "Dude Card"
{
    Caption = 'Dude Card';
    PageType = Card;
    SourceTable = dude;

    layout
    {
        area(content)
        {
            group(General)
            {
                field(No; Rec.No)
                {
                    ApplicationArea = All;
                }
                field(Name; Rec.Name)
                {
                    ApplicationArea = All;
                }
                field(City; Rec.City)
                {
                    ApplicationArea = All;
                }
            }
        }
    }
}

The Right Way: Use NoSeriesManagement Codeunit

When you want to get the next number from a Number Series, you might be tempted to directly query the No. Series and No. Series Line tables, figure out the last used number, and increment it yourself. Don’t do that.

Instead, use Codeunit 396 — NoSeriesManagement. Erik acknowledges that this codeunit is… extensive. It contains many procedures, some deprecated, and could benefit from refactoring. But the good news is that it boils down to one key function:

NoMgt.GetNextNo(NumberSeriesCode, Date, ModifySeries)

The parameters are:

  • NumberSeriesCode (Code[20]) — the code of the Number Series to use (stored in your setup table).
  • Date (Date) — typically WORKDATE or TODAY. This matters if your Number Series has date-based lines (e.g., different number ranges for different months or periods).
  • ModifySeries (Boolean) — when true, the function increments the “Last No. Used” on the series line. When false, it only peeks at what the next number would be without consuming it.

This single function handles all the complexity of finding the right line, incrementing the number, and respecting date ranges.

Creating a Setup Table

Following the standard Business Central pattern, you need a setup table to store which Number Series your app should use. This is the classic “single-record setup table” pattern with a blank Code[10] primary key:

table 54301 "Dude Setup"
{
    Caption = 'Dude Setup';
    DataClassification = ToBeClassified;

    fields
    {
        field(1; PKEY; Code[10])
        {
            Caption = 'PKEY';
            DataClassification = ToBeClassified;
        }
        field(2; "No. Series for Dude"; Code[20])
        {
            Caption = 'No. Series for Dude';
            DataClassification = ToBeClassified;
            TableRelation = "No. Series".Code;
        }
    }
    keys
    {
        key(PK; PKEY)
        {
            Clustered = true;
        }
    }
}

The corresponding setup page auto-inserts the single record when opened, so users don’t have to manually create it:

page 54302 "Dude Setup"
{
    Caption = 'Dude Setup';
    PageType = Card;
    SourceTable = "Dude Setup";
    UsageCategory = Administration;
    ApplicationArea = all;

    layout
    {
        area(content)
        {
            group(General)
            {
                field("No. Series for Dude"; Rec."No. Series for Dude")
                {
                    ApplicationArea = All;
                }
            }
        }
    }
    trigger OnOpenPage()
    var
        no: Code[20];
    begin
        if Rec.IsEmpty() then
            Rec.Insert();
        No := 'AAA123123';
        no := IncStr(No);
    end;
}

Erik points out that in older versions of NAV, opening a card bound to an empty single-record table would automatically trigger an insert. That’s no longer the case in Business Central, so you need the OnOpenPage trigger to handle it.

Wiring It All Together

The key piece is the OnInsert trigger on the Dude table. When a new record is inserted and the No. field is blank, it fetches the next number from the configured Number Series:

trigger OnInsert()
var
    Setup: Record "Dude Setup";
    NoMgt: Codeunit NoSeriesManagement;
begin
    if No = '' then begin
        Setup.Get();
        No := NoMgt.GetNextNo(Setup."No. Series for Dude", WORKDATE, true);
    end;
end;

Note that the Setup.Get() call will error if no setup record exists. Erik mentions that in an OnInsert trigger this is acceptable — it simply tells the user that setup is missing. However, if you were in an event subscriber or other code where you don’t control when your code is called, an unprotected Get() would be a bad pattern. In production code, you’d want to wrap this in a meaningful error message.

A Tangent: How IncStr Works

Erik briefly demonstrates the IncStr function, which is the core mechanism behind Number Series incrementing. If you have a string like 'AAA123123', calling IncStr returns 'AAA123124'. It finds the trailing numeric portion of the string and increments it. Business Central loves number codes that combine letters and digits, and IncStr is what makes that work.

Understanding Delayed Insert

Erik explains an important UI behavior: when does the insert actually happen? This is controlled by the DelayedInsert property on pages.

  • When DelayedInsert is false (the default for card pages): the record is inserted into the database as soon as the user moves the cursor from the primary key field to any non-primary key field.
  • When DelayedInsert is true (common on line subpages): the user must leave the entire record before it’s inserted.

In this example, since the Dude Card has one primary key field (No), the record is inserted as soon as the user tabs out of the No. field — even if they tab to the City field. That’s when the OnInsert trigger fires and the number is assigned.

A Note on “Saving” in the UI

Erik points out a subtle trap: Business Central’s UI sometimes shows “Saving…” when you enter data on a setup page, but this doesn’t necessarily mean the data is saved to the database. On the Dude Setup page, the Number Series selection isn’t committed until you actually leave the page. This caught Erik during the demo — the Number Series configuration appeared saved but wasn’t actually persisted yet, causing a “Number Series does not exist” error.

Watch Out: Page.RunModal in Transactions

Erik flags an important warning about the NoSeriesManagement codeunit. If you search for Page.RunModal within it, you’ll find places where the codeunit tries to pop up a UI dialog — for example, to let the user select a Number Series on the fly. This was added to make NAV friendlier for smaller companies (inspired by the C5 product in Denmark).

The problem: there are many places in the application where a number is fetched from a Number Series during an active database write transaction. In those contexts, displaying UI is not allowed. Instead of getting a helpful dialog, you get a large, awkward error message saying:

“Page.RunModal is not allowed in an active write transaction”

Nine out of ten times you see this error, it’s because the NoSeriesManagement codeunit is trying to show that selection dialog in a context where it simply isn’t permitted. It’s a known rough edge that hasn’t been fully cleaned up yet.

Summary

Implementing Number Series in your Business Central AL apps is straightforward once you know the pattern:

  1. Create a setup table with a TableRelation to "No. Series".Code so users can configure which Number Series to use.
  2. Use NoSeriesManagement.GetNextNo() — this is the one function you need. Pass it the series code, a date (typically WORKDATE), and true to consume the number.
  3. Call it in your OnInsert trigger when the primary key field is blank.
  4. Never directly manipulate the No. Series or No. Series Line tables in code.
  5. Be aware of “Allow Gaps” — it’s a performance setting, not a gap-prevention toggle.
  6. Understand DelayedInsert to know exactly when your OnInsert trigger will fire from the UI.