7 min readRishi

Number Sequences in D365 Finance & Operations: Setup, Pitfalls, and X++ Patterns

Number Sequences in D365 Finance & Operations: Setup, Pitfalls, and X++ Patterns

Number sequences are the quiet workhorse of Dynamics 365 Finance & Operations. Every voucher, invoice, and sales order ID comes from one. They are simple to use and surprisingly easy to misconfigure — usually discovered when month-end audit asks why invoice numbers have gaps, or when a high-volume process grinds to a halt waiting on a lock. Here is what is actually happening and how to use the framework well.

A counter scoped to a reference

A number sequence is a counter scoped to a reference. The reference (an EDT registered with the framework) ties the sequence to a piece of data — CustInvoiceId, SalesId, your own custom reference. Scope determines how many counters exist: a sequence scoped to company has one counter per legal entity; scoped shared, one counter for the whole system.

Format is built from segments: a constant prefix, the company segment, and the incrementing number — for example INV-USMF-001234. Editing segments later does not renumber existing records. Plan the prefix before go-live.

The parameters form is where an administrator assigns a real sequence. Code registers the reference. The wizard, or "Generate," creates the assignment. Skip that step and NumberSeqReference::findReference has nothing to increment.

The continuous flag is the architecture

This single setting drives performance and audit behaviour.

ContinuousNon-continuous
Gaps allowedNoYes
Numbers preallocatedNoYes (in blocks)
Performance under loadSlower (locks per number)Fast
Cleanup neededYes (list of consumed/aborted)No
Use forLegally gapless docs (invoices in some regions)Most everything else

Continuous sequences guarantee no gaps, which some tax regimes require for invoices. The cost: the framework tracks every number in a "status list" and must reclaim numbers from aborted transactions. Under heavy concurrency this serializes work. Two users posting invoices wait on the same counter.

Non-continuous sequences preallocate numbers in memory and hand them out without a per-number database lock. They are faster and leave gaps when a transaction rolls back. For anything that does not have a legal gapless requirement, choose non-continuous.

Do not flip a live sequence to continuous to "fix" gaps you already have. The flag changes how future numbers are issued. It does not close last month's holes. If the law requires a gapless series, design it continuous from the first number.

Reserve, commit, or abort

The everyday pattern:

NumberSeq numberSeq = NumberSeq::newGetNum(
    SalesParameters::numRefSalesId());

if (numberSeq)
{
    salesTable.SalesId = numberSeq.num();
}

newGetNum reserves a number. If the surrounding transaction commits, call nothing extra. If it aborts and you want a continuous sequence to reclaim the number, you must signal that:

ttsbegin;
NumberSeq numberSeq = NumberSeq::newGetNum(MyParameters::numRefMyDocId());
myTable.DocId = numberSeq.num();

if (!myTable.validateWrite())
{
    numberSeq.abort();   // returns the number to a continuous sequence
    ttsabort;
}
else
{
    myTable.insert();
    ttscommit;
}

For non-continuous sequences abort() is a no-op — the gap remains, which is fine.

Call abort() in the same transaction you reserved in, before ttsabort. Reserving in one tts and aborting in another leaves a continuous number in the status list with no row owning it. Do not cache a NumberSeq object across form refreshes and call num() again later. Reserve when you write the row.

Wiring a custom table

Three pieces register a new reference. Skip one and the wizard has nothing to assign.

  1. An EDT for your ID, extending the relevant base type. That EDT is the reference. extendedTypeNum(MyDocId) is how every later call finds it.
  2. A NumberSeqModule extension and a loadModule method in your parameters class that registers the reference, its scope, and defaults.
// In your parameters class
protected void numberSeqModuleMyDoc(NumberSeqDatatype _datatype)
{
    _datatype.parmDatatypeId(extendedTypeNum(MyDocId));
    _datatype.parmReferenceHelp(literalStr("My document number"));
    _datatype.parmWizardIsContinuous(false);
    _datatype.parmWizardIsManual(NoYes::No);
    _datatype.parmWizardAllowChangeDown(NoYes::No);
    _datatype.parmSortField(20);
    _datatype.addParameterType(NumberSeqParameterType::DataArea, true, false);
}

public static NumberSequenceReference numRefMyDocId()
{
    return NumberSeqReference::findReference(extendedTypeNum(MyDocId));
}

parmWizardIsContinuous(false) is the default for a custom document that is not a legal invoice. parmWizardIsManual(NoYes::No) stops users typing a colliding value. NumberSeqParameterType::DataArea scopes the counter per company.

  1. Run the number sequence wizard (or generate references) so administrators can assign a real sequence in your parameters form. Then call NumberSeq::newGetNum(MyParameters::numRefMyDocId()) from insert, the same way SalesId is assigned. Forms should go through numRefMyDocId(), not a hard-coded sequence code.

Cleanup is not optional on continuous

Continuous sequences accumulate rows in the consumed/aborted list every time a reservation does not become a committed document. A user cancels a form. A validateWrite fails. A batch retries. Left alone, that list grows. The next reservation spends time walking it. High-volume continuous references get slower over months for this reason, not because the next integer is hard to compute.

Schedule the Clean up number sequences periodic task against the references that are actually continuous. Cleanup reclaims numbers that were reserved and never committed, so the next invoice can reuse them and the gapless promise still holds. Run it when posting is quiet.

Non-continuous sequences do not need this task for correctness. Do not schedule it as a superstition on every reference. Point it at the continuous ones, and check it after a database refresh.

If cleanup has not run in a long time, do not "fix" slowness by switching the reference to non-continuous in production. You will start allowing gaps on a series that may be legally gapless. Run cleanup first.

The AOS recycle gap

Non-continuous sequences cache a block of numbers in memory on the AOS. That is the preallocation that makes them fast. If the server recycles — a deployment, a crash, an IIS reset — the unused tail of that block is gone. The next reservation starts after the block, not after the last number that landed on a row. You see a hole. Expected behaviour.

Do not "fix" it by switching to continuous unless the law requires it. Continuous does not preallocate that way, and it does not give you the throughput you chose non-continuous for. Do not write a job that fills the holes with dummy documents.

Preallocation size is a tradeoff on the sequence. A larger block means fewer database trips and a larger hole on recycle. For a sales order ID, a recycle gap is noise. For a customer-facing document finance reconciles line by line, decide whether the reference should have been continuous before you tune the block.

Do not replace the framework

Manual sequences bypass the counter. If "Manual" is enabled, users type their own values and the framework will happily let two records collide unless a unique index stops the insert. Disable manual on anything that must be unique.

A home-grown max(Id) + 1 in X++ dies under concurrency. Two inserts read the same max. Prefer NumberSeq.

Do not use a continuous sequence for a high-volume, non-legal ID because you dislike gaps. Gaps are the price of the throughput you almost always want.

Production checklist

  • Continuous only where a legal or audit rule forbids gaps. Everything else non-continuous.
  • abort() on the continuous path when validateWrite fails or you ttsabort after newGetNum.
  • Custom table: EDT, loadModule registration, wizard assignment, numRef* accessor, newGetNum at insert.
  • Manual disabled on unique document IDs.
  • Clean up number sequences scheduled for continuous references, and checked after a refresh.
  • Recycle gaps on non-continuous treated as expected, not as a defect.
  • Format segments frozen before the first production number.

Treat the continuous flag as architecture. Keep cleanup scheduled. Prefer NumberSeq. Non-continuous gaps are the price of throughput.

Keep reading

Newsletter

New posts, straight to your inbox

One email when a new post goes up. Double opt-in, unsubscribe anytime. Not a daily digest.

Comments

  • No comments yet. Be the first.