X++ is the backbone of Microsoft Dynamics 365 Finance & Operations (D365 F&O) development. Whether you’re customizing forms, writing batch jobs, or extending standard business logic, the same handful of patterns show up again and again. This post collects 100 of the most useful X++ snippets, organized by category, so you can copy, adapt, and ship faster.
All snippets follow current best practice: extensions and Chain of Command (CoC) instead of direct code modification,
ttsBegin/ttsCommit around writes, and select forUpdate where locking matters.
1. Data Manipulation & CRUD
1. Basic select
CustTable custTable;
select custTable
where custTable.AccountNum == '1101';
2. Select with forUpdate
CustTable custTable;
ttsBegin;
select forUpdate custTable
where custTable.AccountNum == '1101';
custTable.CreditMax = 50000;
custTable.update();
ttsCommit;
3. Insert a record
CustTable custTable;
custTable.clear();
custTable.AccountNum = '1102';
custTable.CustGroup = '10';
custTable.insert();
4. Update using RecordInsertList-free single record
ttsBegin;
custTable.selectForUpdate(true);
custTable.CreditMax += 1000;
custTable.update();
ttsCommit;
5. Delete a record
ttsBegin;
select forUpdate custTable
where custTable.AccountNum == '1102';
custTable.delete();
ttsCommit;
6. Bulk delete_from
delete_from custTrans
where custTrans.AccountNum == '1102'
&& custTrans.TransDate < mkDate(1,1,2020);
7. Bulk update_recordset
update_recordset custTable
setting Blocked = NoYes::Yes
where custTable.CustGroup == '20';
8. insert_recordset
insert_recordset custTableStaging (AccountNum, Name)
select AccountNum, Name from custTable
where custTable.CustGroup == '10';
9. RecordInsertList for fast bulk inserts
RecordInsertList insertList = new RecordInsertList(tableNum(CustTrans));
CustTrans custTrans;
while select custTrans
where custTrans.AccountNum == '1101'
{
CustTrans newTrans = custTrans;
newTrans.setConnection(insertList.dataSource());
insertList.add(newTrans);
}
insertList.insertDatabase();
10. RecordSortedList for grouping in memory
RecordSortedList list = new RecordSortedList(tableNum(CustTrans));
list.sortOrder(fieldNum(CustTrans, TransDate));
11. doUpdate / doInsert to bypass business logic
custTable.doUpdate(); // skips validateWrite / event chain
custTable.doInsert(); // use sparingly, e.g. data upgrade scripts
12. Check RecordId / exists
boolean exists = CustTable::exist('1101');
CustTable ct = CustTable::find('1101');
if (ct.RecId != 0)
{
// record found
}
2. Query & QueryBuildRange
13. Build a query dynamically
Query query = new Query();
QueryBuildDataSource qbds = query.addDataSource(tableNum(CustTable));
qbds.addRange(fieldNum(CustTable, CustGroup)).value(queryValue('10'));
14. Query with a join
QueryBuildDataSource qbdsTrans = qbds.addDataSource(tableNum(CustTrans));
qbdsTrans.relations(true);
qbdsTrans.joinMode(JoinMode::InnerJoin);
qbdsTrans.addLink(fieldNum(CustTable, AccountNum), fieldNum(CustTrans, AccountNum));
15. Run a QueryRun
QueryRun qr = new QueryRun(query);
while (qr.next())
{
CustTable ct = qr.get(tableNum(CustTable));
info(ct.AccountNum);
}
16. Range with multiple values (OR)
qbds.addRange(fieldNum(CustTable, CustGroup)).value(
SysQuery::valueOr('10', SysQuery::valueOr('20', '30')));
17. Range with a “not equal”
qbds.addRange(fieldNum(CustTable, Blocked)).value(
SysQuery::value(NoYes::No));
18. Date range on a query
QueryBuildRange qbr = qbdsTrans.addRange(fieldNum(CustTrans, TransDate));
qbr.value(SysQuery::range(mkDate(1,1,2026), mkDate(12,31,2026)));
19. Dynamic sort order
qbds.addSortField(fieldNum(CustTable, Name), SortOrder::Ascending);
20. Query from a saved query resource (AOT query)
Query query = new Query(queryStr(CustTableListPage));
21. Passing a query to a RunBase / SysOperation class
args.record(custTable);
args.parm(query.pack());
22. QueryFilter helper for form data sources
QueryBuildRange range = SysQuery::findOrCreateRange(
custTable_ds.query().dataSourceTable(tableNum(CustTable)),
fieldNum(CustTable, CustGroup));
range.value(queryValue('10'));
custTable_ds.executeQuery();
3. Number Sequences
23. Get the next number sequence value
NumberSeq numberSeq = NumberSeq::newGetNum(
CustParameters::numRefCustAccount());
str newAccountNum = numberSeq.num();
numberSeq.used();
24. Preview a number without consuming it
NumberSeq numberSeq = NumberSeq::newGetNumFromId(
NumberSeqTable::find(numberSequenceId).RecId, false);
str preview = numberSeq.num();
25. Continuous vs non-continuous check
boolean isContinuous = NumberSeqTable::find(numberSequenceId).Continuous;
26. Number sequence in a data upgrade / migration
NumberSeq numberSeq = NumberSeq::newGetNumFromId(numRefId, true, true);
27. Wrapper method for a custom NumberSeqReference
static NumberSequenceReference numRefMyEntity()
{
return DataTypeUtil::getNumberSequenceReference(extendedTypeNum(MyEntityId));
}
4. Batch Processing & SysOperation Framework
28. Minimal SysOperation service class
class MyOperationService
{
public void process(MyOperationContract _contract)
{
info(strFmt("Processing %1", _contract.parmName()));
}
}
29. SysOperation data contract
[DataContractAttribute]
class MyOperationContract
{
str name;
[DataMemberAttribute('Name')]
public str parmName(str _name = name)
{
name = _name;
return name;
}
}
30. Running a SysOperation as a batch job
SysOperationServiceController controller = new SysOperationServiceController(
classStr(MyOperationService), methodStr(MyOperationService, process),
SysOperationExecutionMode::Synchronous);
controller.parmShowDialog(false);
controller.startOperation();
31. Enable batch on a controller
controller.parmExecutionMode(SysOperationExecutionMode::ReliableAsynchronous);
32. Classic RunBase pattern (legacy, still common)
class MyRunBaseJob extends RunBase
{
public static void main(Args _args)
{
MyRunBaseJob job = new MyRunBaseJob();
if (job.prompt())
{
job.run();
}
}
}
33. Batch task with progress bar
SysOperationProgress progress = new SysOperationProgress();
progress.setTotal(recordCount);
progress.setText("Processing records");
progress.incCount();
34. Check if running in batch
if (RunBase::getCurrentRunningTask())
{
// running in a batch context
}
35. Create a recurring batch job programmatically
BatchHeader batchHeader = BatchHeader::create(
classNum(MyBatchClass), "My recurring job");
batchHeader.save();
5. Forms & Data Sources
36. Data source init override
public void init()
{
super();
custTable_ds.query().dataSourceTable(tableNum(CustTable))
.addRange(fieldNum(CustTable, CustGroup)).value('10');
}
37. Filter a data source at runtime
custTable_ds.query().dataSourceTable(tableNum(CustTable))
.addRange(fieldNum(CustTable, AccountNum)).value(queryValue(accountNum));
custTable_ds.research(true);
38. Refresh and requery a form data source
custTable_ds.research();
custTable_ds.reread();
custTable_ds.refresh();
39. Enable/disable a control based on a field value
public void enabledControls()
{
creditMax.enabled(custTable.CustGroup == '10');
}
40. Set a field as mandatory dynamically
custTable_ds.object(fieldNum(CustTable, CustGroup)).mandatory(true);
41. Lookup override on a form control
public void lookup(FormControl _formControl, str _filterStr)
{
SysTableLookup sysTableLookup = SysTableLookup::newParameters(
tableNum(CustGroup), _formControl);
sysTableLookup.addLookupfield(fieldNum(CustGroup, CustGroup));
sysTableLookup.performFormLookup();
}
42. Validate write on a form data source
public boolean validateWrite()
{
boolean ret = super();
if (custTable.CreditMax < 0)
{
ret = false;
error("Credit max cannot be negative.");
}
return ret;
}
43. Active record and current selection
CustTable current = custTable_ds.cursor();
CustTableList list = custTable_ds.getFirst(true);
44. Iterate all selected (multi-select) records
CustTable selected;
for (selected = custTable_ds.getFirst(true) ? custTable_ds.getFirst(true) : custTable_ds.cursor();
selected;
selected = custTable_ds.getNext())
{
info(selected.AccountNum);
}
45. Open a form and pass a record via Args
Args args = new Args();
args.record(custTable);
args.caller(this);
new MenuFunction(menuItemDisplayStr(CustTable), MenuItemType::Display).run(args);
6. Extensions, Event Handlers & Chain of Command
46. Chain of Command extension on a table method
[ExtensionOf(tableStr(CustTable))]
final class CustTable_MyExtension
{
public void insert()
{
// pre logic
next insert();
// post logic
}
}
47. CoC on a class method
[ExtensionOf(classStr(SalesFormLetter))]
final class SalesFormLetter_MyExtension
{
public void postLoad()
{
next postLoad();
info("Sales form letter posted.");
}
}
48. Pre-event handler (delegate-based)
[PreHandlerFor(classStr(CustTable), methodStr(CustTable, insert))]
public static void CustTable_Pre_insert(XppPrePostArgs args)
{
CustTable custTable = args.getThis();
// validate before insert
}
49. Post-event handler
[PostHandlerFor(classStr(CustTable), methodStr(CustTable, insert))]
public static void CustTable_Post_insert(XppPrePostArgs args)
{
CustTable custTable = args.getThis();
info(strFmt("Customer %1 created.", custTable.AccountNum));
}
50. Subscribing to a SysDataEvent (data event on a table)
[DataEventHandler(tableStr(CustTable), DataEventType::Inserted)]
public static void CustTable_onInserted(Common sender, DataEventArgs e)
{
CustTable custTable = sender as CustTable;
}
51. Field extension (adding a field via extension)
// Defined on the table extension node in the AOT / VS project,
// referenced in code as a normal field once compiled:
custTable.MyCustomField = "ABC";
52. Extending an EDT with a new relation
// Table extension: add EDT-based field, then reference:
CustTable custTable;
custTable.MyRegionId = "EMEA";
53. Extending a SysOperation contract
[ExtensionOf(classStr(MyOperationContract))]
final class MyOperationContract_Extension
{
[DataMemberAttribute('ExtraFlag')]
public boolean parmExtraFlag(boolean _flag = extraFlag)
{
extraFlag = _flag;
return extraFlag;
}
}
54. Class extension adding a new method
[ExtensionOf(classStr(CustVendCustExtension))]
final class CustVendCustExtension_Extension
{
public static str myHelperMethod(CustAccount _accountNum)
{
return _accountNum;
}
}
55. Overriding a form’s CoC method (form extension class)
[ExtensionOf(formStr(CustTable))]
final class CustTable_Form_Extension
{
public void init()
{
next init();
info("Form initialized via extension.");
}
}
7. Exception Handling
56. try/catch with a specific exception
try
{
ttsBegin;
custTable.insert();
ttsCommit;
}
catch (Exception::DuplicateKeyException)
{
error("A customer with this account number already exists.");
}
57. catch with retry
try
{
ttsBegin;
// work
ttsCommit;
}
catch (Exception::UpdateConflict)
{
retry;
}
58. throw a custom error
if (!custTable)
{
throw error(strFmt("Customer %1 not found.", accountNum));
}
59. Custom exception class
class MyCustomException extends Exception
{
}
throw new MyCustomException(Exception::Error, "Custom failure message.");
60. finally block
try
{
// work
}
finally
{
// always runs, e.g. cleanup file handles
}
61. Suppressing infolog warnings during batch validation
SysInfologSuppression suppress = new SysInfologSuppression(Exception::Warning);
// code that might raise warnings
suppress.finally();
8. String, Date & Container Utilities
62. strFmt for string formatting
str message = strFmt("Customer %1 has balance %2", accountNum, balance);
63. String split and join
container parts = str2con("A,B,C", ",");
str joined = con2str(parts, ",");
64. subStr, strLen, strFind
str s = "DynamicsD365";
str sub = subStr(s, 1, 8); // "Dynamics"
int len = strLen(s);
int pos = strFind(s, "D365", 1, len);
65. strLTrim / strRTrim / strTrim equivalents
str trimmed = strLTrim(strRTrim(" hello "));
66. Date arithmetic
TransDate today = systemDateGet();
TransDate nextMonth = addMonths(today, 1);
TransDate eom = endMth(today);
int days = today - startMth(today);
67. Date to string / string to date
str dateStr = date2Str(today, 213, DateDay::Digits2,
DateSeparator::Dash, DateMonth::Digits2, DateSeparator::Dash, DateYear::Digits4);
TransDate parsed = str2Date("2026-08-07", 213);
68. Container basics: pack/unpack values
container c = [accountNum, custGroup, creditMax];
[accountNum, custGroup, creditMax] = c;
69. conFind / conPeek / conIns
int pos = conFind(c, custGroup);
str val = conPeek(c, 2);
c = conIns(c, 4, "extra");
70. Map for key/value lookups
Map map = new Map(Types::String, Types::Integer);
map.insert("A", 1);
if (map.exists("A"))
{
int val = map.lookup("A");
}
71. List and enumerator iteration
List list = new List(Types::String);
list.addEnd("Item1");
list.addEnd("Item2");
ListEnumerator le = list.getEnumerator();
while (le.moveNext())
{
info(le.current());
}
9. Reflection & Metadata
72. Get a table’s field list dynamically
DictTable dictTable = new DictTable(tableNum(CustTable));
int fieldCount = dictTable.fieldCnt();
for (int i = 1; i <= fieldCount; i++)
{
FieldId fieldId = dictTable.fieldCnt2Id(i);
info(dictTable.fieldName(fieldId));
}
73. Get a field’s value by name (reflection)
SysDictField dictField = new SysDictField(tableNum(CustTable), fieldNum(CustTable, AccountNum));
anytype value = custTable.(dictField.id());
74. Check if a table field exists (extension-safe check)
boolean exists = fieldId2Name(tableNum(CustTable), fieldNum(CustTable, MyCustomField)) != "";
75. Instantiate a class dynamically by name
str className = "CustTable";
object obj = SysDictClass::newClassName(className).makeObject();
76. classIdGet / className helpers
int classId = classIdGet(custTable);
str name = classId2Name(classId);
77. Check table type / extends relationship
if (custTable.TableId == tableNum(CustTable))
{
// safe cast context
}
78. Enum reflection (label and value)
str label = enum2str(NoYes::Yes);
int value = enum2int(NoYes::Yes);
NoYes back = str2enum(enumStr(NoYes), "Yes");
10. Security & Workflow
79. Check duty/privilege access
boolean hasAccess = SecurityRights::hasAccess(
identifierstr(CustTableMaintain), AccessType::Update);
80. Table-level permission check
if (!CustTable::checkAccess(AccessType::Delete))
{
error("You do not have permission to delete customers.");
}
81. Record-level security query
// Defined via Security Policy in AOT; enforced automatically once
// the policy's query is linked to the table's primary data source.
82. Submit a document to workflow
Workflow::activateFromWorkflowType(
WorkflowTypeName, custTable.RecId, "Submitted for approval", NoYes::No, custTable.TableId);
83. Check current workflow state of a record
WorkflowTrackingStatusTable status;
select firstonly status
where status.ContextRecId == custTable.RecId;
84. Get current user id and company
UserId user = curUserId();
CompanyId company = curExt();
85. Run code in another company context
changecompany('DAT')
{
CustTable custTable;
select custTable where custTable.AccountNum == '1101';
}
11. Everyday Utilities
86. Write to the Infolog
info("Process completed successfully.");
warning("Check the customer credit limit.");
error("Operation failed.");
87. Temporary table usage
MyTmpTable tmpTable;
tmpTable.Name = "Temp row";
tmpTable.insert();
while select tmpTable
{
info(tmpTable.Name);
}
88. RunBaseTmpTableService for returning a temp table to a form
[SysEntryPointAttribute]
public MyTmpTable getTmpData()
{
MyTmpTable tmp;
tmp.Name = "Example";
tmp.insert();
return tmp;
}
89. Number formatting
str formatted = num2str(1234.5, 0, 2, DecimalSeparator::Comma, ThousandSeparator::Period);
90. Reading a configuration key / license state
if (isConfigurationKeyEnabled(configurationKeyNum(LedgerBasic)))
{
// logic gated by config key
}
91. Caching a lookup value with a static Map
class MyCache
{
static Map cache;
static str getGroupName(CustGroupId _id)
{
if (!cache)
{
cache = new Map(Types::String, Types::String);
}
if (!cache.exists(_id))
{
CustGroup custGroup = CustGroup::find(_id);
cache.insert(_id, custGroup.Name);
}
return cache.lookup(_id);
}
}
92. Read and write a file (streams)
System.IO.StreamWriter writer = new System.IO.StreamWriter(@"C:\Temp\out.txt");
writer.WriteLine("Hello from X++");
writer.Close();
93. Export data to a CSV via a text streaming class
CommaTextStreamIo file = CommaTextStreamIo::constructForWrite();
file.write(["AccountNum", "Name"]);
file.write([custTable.AccountNum, custTable.Name]);
94. Call an external web service (basic HttpClient usage)
System.Net.Http.HttpClient client = new System.Net.Http.HttpClient();
System.Threading.Tasks.Task response = client.GetAsync("https://example.com/api");
95. Multi-threaded / async task pattern (SysOperation async)
SysOperationServiceController controller = new SysOperationServiceController(
classStr(MyOperationService), methodStr(MyOperationService, process),
SysOperationExecutionMode::ReliableAsynchronous);
controller.startOperation();
96. Global function-style static method (Global class pattern)
[ExtensionOf(classStr(Global))]
final class Global_MyExtension
{
public static str myGlobalHelper(str _input)
{
return strUpr(_input);
}
}
97. Using #define / macros
#define.MaxRetries(3)
for (int i = 1; i <= #MaxRetries; i++)
{
// retry logic
}
98. Using label references instead of hardcoded strings
str message = "@MyModule:CustomerCreatedSuccessfully";
info(message);
99. Performance: avoid record-by-record loops with SysOperationExecutionMode + set-based ops
// Prefer this:
update_recordset custTable setting Blocked = NoYes::Yes
where custTable.CustGroup == '20';
// Over row-by-row updates in a while-select loop.
100. Wrapping a whole operation in a single transaction
ttsBegin;
try
{
custTable.insert();
custTrans.insert();
ttsCommit;
}
catch
{
ttsAbort;
throw;
}


