Why this.proforma() Is Not Reliable in Batch and Why prepareProforma() Exists in D365 Finance and Operations
When working with the FormLetter framework in Dynamics 365 Finance & Operations (and AX 2012+), it is a common assumption that this.proforma() can be used to reliably detect whether the current posting run is a proforma. While this assumption generally holds true for interactive (non‑batch) executions, it breaks down in batch and multithreaded scenarios.
The reason is architectural.
Proforma postings are transactional and disposable
This rollback behavior already makes proforma detection fragile if your logic depends on state after posting.
Batch execution changes the call stack and lifecycle
SalesFormLetter_*instances may be reusedafterOperationBody()may execute outside the scope where the proforma flag is meaningful- The boolean behind
this.proforma()can be reset, defaulted, or evaluated too late
Microsoft explicitly introduced prepareProforma() for batch correctness
FormletterService.prepareProforma()- It is invoked only for proforma runs
- It executes per journal being processed
- It runs at the correct time in the batch lifecycle, before posting/rollback occurs
- It is safe for multi‑threaded execution
[ExtensionOf(classStr(SalesFormLetter_Confirm))]
final class SalesFormLetter_Confirm_StoneridgeSoftware_Extension
{
protected void afterOperationBody()
{
// This is a manual/UI run.
// For batched/multithreaded SO Confirm, see FormletterService_StoneridgeSoftware_Extension
next afterOperationBody();
if (this.proforma() && !this.isInBatch())
{
Common sourceTable = this.parmSourceTable();
if (sourceTable && sourceTable.TableId == tableNum(SalesTable))
{
SalesTable salesTable = sourceTable as SalesTable;
// TODO ACTION
}
}
}
}
[ExtensionOf(classStr(FormletterService))]
final class FormletterService_StoneridgeSoftware_Extension
{
// prepareProforma is called only during batched run, and it is per journal post being processed
protected void prepareProforma(FormletterJournalPost _formletterJournalPost)
{
// Handle SO Confirmations
if(this.documentStatus == DocumentStatus::Confirmation && this.formletterType == formletterType::Sales)
{
ParmId parmIdLoc = this.parmId;
SalesTable salesTable;
SalesParmSubTable salesParmSubTable;
ttsbegin;
while select forupdate salesTable
join salesParmSubTable
where salesParmSubTable.ParmId == parmIdLoc
&& salesTable.SalesId == salesParmSubTable.OrigSalesId
{
// TODO ACTION
}
ttscommit;
}
next prepareProforma(_formletterJournalPost);
}
Under the terms of this license, you are authorized to share and redistribute the content across various mediums, subject to adherence to the specified conditions: you must provide proper attribution to Stoneridge as the original creator in a manner that does not imply their endorsement of your use, the material is to be utilized solely for non-commercial purposes, and alterations, modifications, or derivative works based on the original material are strictly prohibited.
Responsibility rests with the licensee to ensure that their use of the material does not violate any other rights.

