X++ Snippets Every D365 Developer Should Know

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;
}

Wrapping Up

These 100 snippets cover the patterns you’ll reach for most often in day-to-day D365 F&O development: CRUD and set-based data operations, dynamic queries, number sequences, batch and SysOperation jobs, form customization, extensions and event handlers, error handling, string/date/container utilities, reflection, and security/workflow basics. Bookmark this list as a quick reference, and prefer extensions and Chain of Command over direct AOT modification wherever possible to keep your code upgrade-safe.
Scroll to Top