4 min readRishi

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.

The model

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.

Continuous vs non-continuous — the decision that matters

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 it hands out in a "status list" and must reclaim numbers from aborted transactions. Under heavy concurrency this serializes work and becomes a bottleneck.

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

Using a number sequence in X++

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 simply remains, which is fine.

Adding a number sequence to a custom table

Three pieces wire a new reference into the framework:

  1. An EDT for your ID, extending the relevant base type.
  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));
}
  1. Run the number sequence wizard (or generate references) so administrators can assign a real sequence in your parameters form.

Pitfalls that reach production

  • Preallocation "loses" numbers on AOS restart. Non-continuous sequences cache a block; if the server recycles, the unused tail of that block is gone, creating a gap. Expected behaviour — do not "fix" it by switching to continuous unless the law requires it.
  • The status list grows unbounded. Continuous sequences accumulate rows in the consumed/aborted list. Schedule the Clean up number sequences periodic task, or high-volume continuous references bloat and slow down.
  • Manual sequences bypass the counter. If "Manual" is enabled, users type their own values and the framework will happily let two records collide. Disable manual on anything that must be unique.
  • Changing the format mid-life. Editing segments on a live sequence does not renumber existing records; it only affects new ones. Plan format up front.

Treat the continuous flag as an architectural decision, keep the cleanup task scheduled, and prefer the framework's NumberSeq API over any clever home-grown counter. The gaps you will see in non-continuous sequences are not bugs — they are the price of the throughput you almost always want.

Keep reading

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

No comments yet. Be the first.