name: xpp-best-practice-rules
description: Best-practice (BP) rules every generated X++ file must satisfy — today/DateTimeUtil, label-typed messages, EDT migration, nested loops, alternate keys, doc comments, label existence. Invoke whenever you scaffold or edit X++ that will eventually be linted by xppbp.exe.
applies_when: User intent involves authoring X++ that must pass d365fo bp check / xppbp.exe — i.e. virtually all D365FO code.
⛔ NEVER write X++ AOT XML files directly via PowerShell, terminal file commands (
Set-Content,Out-File,New-Item), editor write tools, or any raw text approach. The XML schema (<AxClass>,<AxTable>,<AxForm>,<Methods>,<SourceCode>) is proprietary — LLMs have not been trained on it reliably. ALWAYS used365fo generate …commands to produce correct AOT XML. Ifd365fois unavailable in PATH, stop and ask the user to install it.
Best-practice rules — generated X++ must be BP-clean
Source of truth:
d365fo bp check— the Windows-VM runner that executesxppbp.exe. The list below covers the non-negotiable BP rules every scaffold and hand-edit must satisfy out of the box.
Per-rule rules
BPUpgradeCodeToday — today() is forbidden
today() ignores the user's preferred time-zone. Always use:
TransDate today = DateTimeUtil::getToday(DateTimeUtil::getUserPreferredTimeZone());
BPErrorLabelIsText — no hardcoded UI strings
Every string passed to info() / warning() / error() / Box::yesNo() etc. must be a label token of the form @File:Key. Search before you create:
d365fo labels search "Vehicle is required" --lang en-us --output json
d365fo labels resolve @SYS12345 # confirm an existing token
If no match → create the label via your model's labels file, then reference it. Never inline.
BPErrorEDTNotMigrated — modern EDT relations
EDT relations must use the EDT.Relations element, not the legacy table-level relations on the EDT. The CLI's d365fo generate edt and d365fo generate extension edt already emit the modern shape — preserve it when hand-editing.
BPCheckNestedLoopinCode — no nested data-access loops
Nested while select blocks are forbidden:
// ❌ WRONG
while select custTable
{
while select custInvoiceJour where custInvoiceJour.OrderAccount == custTable.AccountNum
{ … }
}
// ✅ CORRECT — single join
while select custTable
join custInvoiceJour
where custInvoiceJour.OrderAccount == custTable.AccountNum
{ … }
For filter-only joins use exists join / notExists join. For complex correlations pre-load to a Map or temp table.
BPCheckAlternateKeyAbsent — every table needs an alternate key
A unique index on the natural key, marked AlternateKey = Yes. The CLI's d365fo generate table template emits a <Pkey> index with AlternateKey = Yes — don't strip it.
BPErrorUnknownLabel — labels referenced must exist
@File:Key tokens must resolve to a real entry in an indexed label file. Confirm with:
d365fo labels resolve @File:Key --lang en-us,cs --output json
If the result is ok:false with LABEL_NOT_FOUND, stop and either pick an existing label (d365fo labels search …) or add the entry to the model's labels file before referencing it.
BPXmlDocNoDocumentationComments — meaningful doc comments
Public/protected classes and methods need a non-trivial /// <summary>:
/// <summary>Calculates the customer balance in the company currency.</summary>
/// <param name="_includeOpen">Whether open transactions count.</param>
/// <returns>Balance in MST.</returns>
public AmountMST balanceMST(boolean _includeOpen = true) { … }
Auto-generated stubs ("This method does foo.") do not count. Restate the contract.
BPDuplicateMethod — no dupes on the inheritance chain
Adding a method that already exists on a base class in the same model fails BP. Run d365fo get class <Base> to confirm before adding.
Label-on-field exception
When adding a field whose EDT already carries a label, do NOT set --label on the field — it inherits from the EDT. Override only if you deliberately want a different caption in this table:
d365fo generate table FmVehicle \
--field VIN:VinEdt:mandatory \ # ← VinEdt has Label = "VIN" — leave it alone
--field Make:Name \ # ← inherits "Name" from EDT
--label "@Fleet:Vehicle"
Linting workflow
# In-process heuristics — fast, runs anywhere, useful for CI:
d365fo lint --output sarif > lint.sarif
# Run specific method-flag categories (detected at index-extract time):
d365fo lint --category today-usage # BPUpgradeCodeToday: today() calls
d365fo lint --category do-insert-update # doInsert/doUpdate/doDelete usage
d365fo lint --category doc-comment-missing # BPXmlDocNoDocumentationComments
# Full BP — only on the Windows VM, only on user request:
d365fo bp check --output json
Six categories are now available: table-no-index, ext-named-not-attributed, string-without-edt, today-usage, do-insert-update, doc-comment-missing. The method-flag categories are populated at extract time by scanning <Source> text — no full body is stored. Re-run d365fo index refresh after editing source before linting.
Never auto-run bp check. It blocks the user (slow, Windows-only). Say "Changes scaffolded. Run d365fo bp check when you're ready."
Hard "never" list
- Never call
today(). - Never hardcode a UI string in
info()/warning()/error(). - Never nest
while selectblocks. - Never ship a table without an alternate key.
- Never reference a label without verifying it exists.
- Never auto-run
d365fo bp check.
CLI object-discovery best practices
When the user asks a functional question — "which classes print a free invoice", "which classes process sales orders" — translate it into 2–4 English keyword fragments and run a single targeted batch search. Always add --kind class (or the relevant kind) to avoid scanning all 13 object types.
# ✅ CORRECT — kind-filtered, fast
d365fo search batch FreeTextInvoice PrintFreeTxt FreeInv --kind class --output json
# ❌ WRONG — full scan across Tables/Classes/EDTs/Enums/Forms/… slow and noisy
d365fo search batch FreeTextInvoice PrintFreeTxt FreeInv --output json
- Use at most 4 fragments per call. More fragments add tokens, not precision.
- Use the English AOT name (
FreeTextInvoice, notvolná faktura). - If the result set has >50 hits, refine with a tighter fragment instead of adding more queries.
- After identifying candidate names, resolve details with
d365fo get class <Name> --output json.