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.
| Continuous | Non-continuous | |
|---|---|---|
| Gaps allowed | No | Yes |
| Numbers preallocated | No | Yes (in blocks) |
| Performance under load | Slower (locks per number) | Fast |
| Cleanup needed | Yes (list of consumed/aborted) | No |
| Use for | Legally 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:
- An EDT for your ID, extending the relevant base type.
- A
NumberSeqModuleextension and aloadModulemethod 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));
}
- 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
Extending Data Entities in D365 Finance & Operations Without Breaking Upgrades
Add fields, computed columns, and validation to standard D365 Finance & Operations data entities the upgrade-safe way — with X++ examples and the staging-table traps to avoid.
Chain of Command vs Event Handlers: Extending D365 F&O the Right Way
When to use Chain of Command and when to use pre/post event handlers in Dynamics 365 Finance & Operations — with X++ examples, a decision table, and the gotchas that trip up teams.
Electronic Reporting in D365 Finance: Building Custom Formats Without Code
A practical guide to the Electronic Reporting (ER) framework in D365 Finance — data models, model mappings, and format configurations to produce custom files without X++.
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.