Skip to main content

Batch job status changed

Fires when the status of a batch job is changed through the job record - for example when an operator withholds, releases, or cancels a job from the Batch jobs page - unless the job matches one of your batch job event filters, which filter out the jobs your subscribers don't need. It carries the previous and new status, the caption, legal entity, start and end times, and task count. Read the firing condition below before relying on it to detect job completion - the batch server's own start and finish transitions don't raise it.

Event classMGCLatticeBatchJobStatusChangedBusinessEvent
Contract classMGCLatticeBatchJobStatusChangedBusinessEventContract
CategorySystem administration
ModuleModuleAxapta::SystemAdministration
Label IDsBatchJobStatusChangedBusinessEventName / BatchJobStatusChangedBusinessEventDescription
TriggerBatchJob.update() -> Status differs from orig().Status and MGCLatticeBatchJobEventFilter::excludes() is false

Trigger / Hook​

  • Extension class: MGCLatticeBatchJob_Table_Extension, decorated with [ExtensionOf(tableStr(BatchJob))].
  • CoC method signature: public void update().
  • Firing condition: after next update(), the event sends when all three hold: the new Status differs from orig().Status; the event is activated; and no enabled MGCLatticeBatchJobEventFilter row matches the job (see Filter setup). One status change raises at most one event.
  • Company: BatchJob is shared, so the activation check, the filter lookup and the send run in the job's company (BatchJob.Company; the session company when the job has none or the company no longer exists). The event is published when that company has activated it, and BusinessEventLegalEntity is the job's company.
  • Which status changes reach this hook: only changes written through the BatchJob record's update() method.
    • Raised: Change status on the Batch jobs page (BatchChangeStatus) - to Withhold (from Waiting, Ended, Error, or Canceled), to Waiting (from Withhold, Ended, Error, or Canceled), or to Canceling (from Executing).
    • Raised: the platform's clean-up of jobs whose run-as user (the job's ExecutingBy user, or else its creator) is disabled or removed - BatchRun::serverCleanUpDeadTasks(), Executing -> Canceling and Waiting -> Withhold. It uses update_recordset without skipping table methods, and because the platform BatchJob table overrides update(), the statement runs record by record through this hook.
    • Raised: any other code that sets BatchJob.Status and calls update() on the record.
    • Not raised: the batch framework's own scheduling transitions. Waiting -> Executing when a job starts, Executing -> Ended or Error when it finishes, Canceling -> Canceled when a cancellation completes, and the reset of recurring jobs are written with set-based updates under skipDataMethods(true) (and, for very large jobs, by SQL stored procedures), all of which bypass update(). The event therefore never reports those transitions, and a filter row whose On status is Executing, Ended, Error, or Canceled only affects status changes made through the record.
  • Timing: BatchChangeStatus updates the job inside a transaction, so send() writes to the business events commit log and the event is delivered only after that transaction commits.
  • Enablement gate: BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeBatchJobStatusChangedBusinessEvent)), a cached lookup. It is checked first for the job's company and the session company, before any database read, and again in the job's company before the filter lookup.
public void update()
{
BatchStatus origStatus = this.orig().Status;

next update();

if (origStatus != this.Status
&& (BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeBatchJobStatusChangedBusinessEvent), this.Company ? this.Company : curExt())
|| BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeBatchJobStatusChangedBusinessEvent))))
{
changecompany(MGCLatticeBusinessEventCompany::resolve(this.Company))
{
if (BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeBatchJobStatusChangedBusinessEvent))
&& !MGCLatticeBatchJobEventFilter::excludes(this))
{
MGCLatticeBatchJobStatusChangedBusinessEvent::newFromBatchJob(this, origStatus).send();
}
}
}
}

Filter setup​

Filters live in the MGCLatticeBatchJobEventFilter table, maintained at System administration > Setup > Lattice business events > Batch job event filters (menu item MGCLatticeBatchJobEventFilter; access is granted by the MGCLatticeBatchJobEventFilterMaintain privilege). The table is shared across companies, so each row applies to batch jobs in every company. The rows are exclusions: a job that matches an enabled row is filtered out, and its status changes don't raise the event.

FieldMeaning
EnabledOnly rows with Enabled = Yes are evaluated. New rows default to Yes.
DescriptionFree text for your own reference; not used for matching.
Caption containsCase-insensitive substring that the batch job's caption must contain for the row to match. Blank matches any caption.
Task classAOT class name; the row matches only jobs that contain at least one task of this class. Blank matches any job. A name that doesn't resolve to a class never matches.
Match any statusYes = the row matches whatever the new status is. New rows default to Yes.
On statusUsed only when Match any status = No: the row matches only when the job's new status equals this value.

Each combination of Caption contains, Task class, Match any status and On status can be entered only once.

How rows combine (MGCLatticeBatchJobEventFilter::excludes() / rowMatches()):

  • Within a row, every specified criterion must match (AND); a blank criterion is a wildcard.
  • Across rows, any one matching enabled row filters the job out (OR).
  • With no enabled rows, nothing is filtered out: every status change that reaches the hook raises the event.

Examples: a row with Caption contains = Invoice and Match any status = Yes stops the event for every job whose caption contains "invoice"; a row with Match any status = No and On status = Withhold (Hold) stops it for jobs being withheld, while their other status changes still raise it.

Payload Contract​

One row per [DataMember], in declaration order. Sources are the BatchJob record at its new status, plus the status it held before the update, mapped in initialize().

JSON fieldDisplay labelType / EDTSourceNotes
batchJobRecIdBatch Job Record IDint64BatchJob.RecId
captionCaptionstrBatchJob.Caption
legalEntityLegal EntityDataAreaIdBatchJob.CompanyCompany the job belongs to, in lower case (BatchJob.Company keeps the case it was written with)
statusStatusintBatchJob.Status (as integer)BatchStatus: 0 Hold (Withhold), 1 Waiting, 2 Executing, 3 Error, 4 Finished (Ended), 5 Ready, 6 NotRun, 7 Canceling (Canceling), 8 Canceled, 9 Scheduled
statusNameStatus NamestrBatchJob.Status (enum symbol)The symbol, not the UI label - for example "Hold" for Withhold and "Cancelling" for Canceling
previousStatusPrevious StatusintBatchJob.orig().Status (as integer)Same values as status
previousStatusNamePrevious Status NamestrBatchJob.orig().Status (enum symbol)
startDateTimeStart Date TimeStartDateTimeBatchJob.StartDateTime\/Date(ms)\/ form; an unset value serializes as \/Date(-2208988800000)\/ (1900-01-01)
endDateTimeEnd Date TimeEndDateTimeBatchJob.EndDateTime\/Date(ms)\/ form; unset while a job is running
taskCountTask CountintCount of Batch rows where BatchJobId = the job's RecIdNumber of tasks in the job

Every Lattice event also carries the Lattice ordering and correlation envelope, inherited from MGCLatticeBusinessEventContractBase: eventSequence (Int64 - increasing across the whole catalog and all companies, allocated when the event is raised; it orders the events of one record, gaps are normal and permanent, see the README), correlationId (String - always Scope:Key; groups emissions that belong to one logical run and defaults to Event: plus a GUID unique to this emission) and schemaVersion (Int - the payload shape version; 1 for this contract). D365 F&O also adds its standard envelope fields to every business event payload: BusinessEventId, BusinessEventLegalEntity, ContextRecordSubject, ControlNumber, EventId, EventTime, EventTimeIso8601, InitiatingUserAADObjectId, MajorVersion, MinorVersion, and ParentContextRecordSubjects. Do not rely on the order of members in the delivered JSON.

This event doesn't call correlateTo, so correlationId is unique to each emission.

Example Payload​

An operator cancels a running job from the Batch jobs page:

{
"BusinessEventId": "MGCLatticeBatchJobStatusChangedBusinessEvent",
"ControlNumber": 5637151340,
"EventId": "0B7D4E19-3A62-4C8F-95E1-7F2C8A6D4B30",
"EventTime": "\/Date(1790257287000)\/",
"EventTimeIso8601": "2026-09-24T13:41:27.9887517Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{577C3196-7236-B014-2312-29B924A43BF7}",
"ParentContextRecordSubjects": [],
"eventSequence": 48766,
"correlationId": "Event:6e2f9a41-0c57-4b3d-a8e6-19d4c7b2f085",
"schemaVersion": 1,
"batchJobRecId": 5637148901,
"caption": "Invoice sales orders - nightly",
"legalEntity": "usmf",
"status": 7,
"statusName": "Cancelling",
"previousStatus": 2,
"previousStatusName": "Executing",
"startDateTime": "\/Date(1790255100000)\/",
"endDateTime": "\/Date(-2208988800000)\/",
"taskCount": 3
}

Activation​

In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Batch job status changed") or by filtering the Category column to System administration. Select it and use Activate to bind it to a configured endpoint (for example, an Azure Service Bus queue, Event Grid topic, or HTTPS webhook).

Filter rows are optional. Without any, every batch job status change that reaches the hook raises the event; to leave out jobs your subscribers don't need, add enabled rows at System administration > Setup > Lattice business events > Batch job event filters (see Filter setup).