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.
| 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 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.
- An EDT for your ID, extending the relevant base type. That EDT is the reference.
extendedTypeNum(MyDocId)is how every later call finds it. - 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));
}
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.
- 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())frominsert, the same waySalesIdis assigned. Forms should go throughnumRefMyDocId(), 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 whenvalidateWritefails or youttsabortafternewGetNum.- Custom table: EDT,
loadModuleregistration, wizard assignment,numRef*accessor,newGetNumat 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
update_recordset: SQL Ran, and update() Never Did
update_recordset, insert_recordset, and delete_from send SQL now. They skip table methods, CoC, and events. Stamp the columns the method would have stamped, or stay on the row path.
FormDataSource.research: The Range You Added Never Ran
research reruns the live QueryRun. A range on query() waits for executeQuery. reread reloads only the current RecId, so an insert stays off the grid.
CacheLookup in X++: Found Serves the Stale Row Inside ttsbegin
Found serves the same CustTable inside ttsbegin. NotInTTS goes to SQL. EntireTable is for parameters. No unique index means the cache never hits.
crossCompany in X++: The Query That Mixes Legal Entities
A normal select stays in the current company. crossCompany returns every legal entity the user may read. How the company container, changeCompany, and OData differ.
Budget Control in Finance 10.0.49: Closed Periods, Reservations, and Broken Tracking
The budgeting features in Dynamics 365 Finance 10.0.49 — accounting-date advancement, reservation posting, encumbrance reporting, and a diagnostic for corrupt budget tracking.
Bank and Cash in Finance 10.0.49: Delayed Settlement, Match Preview, and Bridge Selection
What Dynamics 365 Finance 10.0.49 changes in payment journals and bank reconciliation, which pieces are still preview, and which bank behaviors you can no longer disable.
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.