Technical6 min read

SysOperation batch job in X++: step by step

SysOperation batch job in D365 F&O: Contract, Service, Controller and action menu item. Sample X++ code, retries and common mistakes.

The short answer

A batch job with the SysOperation framework relies on three classes and one menu item: a Contract class (the parameters, shown automatically in the dialog), a Service class (the business logic), a Controller that extends SysOperationServiceController (it links the service, the method and the execution mode), and an action menu item pointing to the Controller. The user then runs the job immediately or schedules it in batch from the dialog.

SysOperation separates parameters, logic and execution, whereas the older RunBaseBatch model mixed everything in one class. Microsoft's "real async" feature also builds on it.

Step 1: the Contract class

Example: purge a custom integration log (table MyIntegrationLog) before a given date. The Contract holds one parameter, the cut-off date. Each parm method decorated with DataMemberAttribute becomes a field in the dialog.

[DataContractAttribute]
class MyPurgeLogContract
{
    TransDate cutOffDate;

    [DataMemberAttribute('CutOffDate')]
    public TransDate parmCutOffDate(TransDate _cutOffDate = cutOffDate)
    {
        cutOffDate = _cutOffDate;
        return cutOffDate;
    }
}

Step 2: the Service class

The service method receives the Contract as a parameter. Here, a delete_from removes all matching rows in a single SQL statement, rather than a loop deleting row by row: see our article on insert_recordset, update_recordset and delete_from.

class MyPurgeLogService extends SysOperationServiceBase
{
    public void process(MyPurgeLogContract _contract)
    {
        MyIntegrationLog log;
        TransDate        cutOffDate = _contract.parmCutOffDate();

        ttsbegin;
        delete_from log
            where log.LogDate < cutOffDate;
        ttscommit;
    }
}

Step 3: the Controller and the menu item

The Controller extends SysOperationServiceController. Its constructor takes the service class name, the method name and the execution mode; the main method passes the arguments and starts the operation.

class MyPurgeLogController extends SysOperationServiceController
{
    public static void main(Args _args)
    {
        MyPurgeLogController controller = new MyPurgeLogController(
            classStr(MyPurgeLogService),
            methodStr(MyPurgeLogService, process),
            SysOperationExecutionMode::Synchronous);

        controller.parmArgs(_args);
        controller.startOperation();
    }
}

Then create an action menu item (ObjectType = Class, Object = MyPurgeLogController), add it to a privilege linked to a role, then to the module menu. In the dialog, the background processing tab lets the user schedule the run in batch (recurrence, batch group).

Retries, parameter versions and real async

Three points documented on Microsoft Learn:

  • Retries: a batch task's "Maximum retries" field reruns the task after an error or a batch server restart. For transient SQL errors, implement the BatchRetryable interface (isRetryable method) on the Controller, or set parmIdempotent(true) on batchInfo. Only enable this if the job can be replayed without side effects; a date-based purge is such a case.
  • Parameter versions: if you add a parameter to a class whose parameters are serialised (pack/unpack), handle the old versions to avoid the error "An error occurred while unpacking parameters for batch job".
  • Real async: a feature (in preview) lets some SysOperation operations run asynchronously without blocking the user, by overriding canRunAsRealAsync and getSysRealAsyncOperationId in the Controller.

Common mistakes

  • Putting business logic in the Controller: it belongs in the Service, testable on its own.
  • Forgetting DataMemberAttribute on a parm method: the parameter does not appear in the dialog and is not serialised.
  • Looping with while select … delete() over thousands of rows instead of a delete_from.
  • Marking a non-replayable job as BatchRetryable or idempotent: a retry can then create duplicates.
  • Forgetting the privilege on the menu item: the job exists but nobody can run it.
  • Not versioning parameters after a change: already scheduled batches fail when unpacking.

Having the batch job generated

Contract, Service, Controller, menu item, privilege: the pattern is repetitive and lends itself to generation. Skalp AI analyses the ERP context, writes the specification, generates the X++ code and unit tests, then delivers to your Azure DevOps for validation by your IT lead. See D365 F&O batch and SysOperation jobs, the bigger picture in building X++ without a consultant in 2026, and the Basic, Pro and Max plans (1 credit, ≈ €400 on Basic, €200 on Pro, consumed only when the code is generated).

Sources

Frequently asked questions

SysOperation or RunBaseBatch?

For new development, SysOperation: it separates parameters, logic and execution, and recent features such as real async build on it. RunBaseBatch remains in a lot of existing code.

How do you make a SysOperation batch retryable after a SQL error?

Implement BatchRetryable (isRetryable returns true) on the Controller, or call batchInfo().parmIdempotent(true). The job must be replayable without creating duplicates.

Why "An error occurred while unpacking parameters for batch job"?

A scheduled batch's serialised parameters no longer match the current version of the class. Microsoft recommends versioning the parameter list and handling each version in unpack.

Ready to transform your D365?

Join the CIOs who develop their D365 tickets in 1h for 1 credit (≈ €400 on Basic, €200 on Pro).