Skip to main content

Maintenance work order completed

Fires when an Asset Management work order moves into a lifecycle state that has Set actual end enabled, the state that stamps the work order's actual end date and time (in the standard USMF demo data, Completed). Work order lifecycle states are defined by each customer, so the event follows that option on the target state, not the state's name. Downstream systems can use it to close the job in field-service or CMMS tools, stop downtime clocks and return the equipment to service in MES or SCADA, or trigger cost and KPI reporting.

Event classMGCLatticeMaintenanceWorkOrderCompletedBusinessEvent
Contract classMGCLatticeMaintenanceWorkOrderCompletedBusinessEventContract
CategoryAsset management
ModuleModuleAxapta::AssetManagement
Label IDsMaintenanceWorkOrderCompletedBusinessEventName / MaintenanceWorkOrderCompletedBusinessEventDescription
TriggerLifecycle state update (EntAssetLifecycleStateUpdate_WorkOrderTable.updateStateRecord()) -> lifecycle state changes to a state with Set actual end = Yes

Trigger / Hook​

  • Extension class: MGCLatticeEntAssetLifecycleStateUpdate_WorkOrderTable_Class_Extension, [ExtensionOf(classStr(EntAssetLifecycleStateUpdate_WorkOrderTable))], wrapping protected void updateStateRecord(RecId _recId) - a process hook on the lifecycle state update, not a table data event. It is the only standard code that changes a work order's lifecycle state: the Update work order state action, work order scheduling (through EntAssetWorkOrderTable.setState()), unscheduling and the mobile app all run it.
  • The wrapper reads the work order's state before next and calls MGCLatticeMaintenanceWorkOrderEventHandlers::raiseForLifecycleTransition() afterwards, which reads the work order again. The same helper also raises the Scheduled and Started events; the decision is made by eventForTransition().
  • Other writers: work orders written by data entities, imports or code outside the lifecycle state update do not raise the event, even when they change the state.
  • Firing condition:
    1. The lifecycle state update must change the work order's lifecycle state (the state read after next differs from the one read before it), and the work order must already have had a state. Updates that leave the state unchanged are ignored, and the first state a work order receives while it is created is reported by the Created event instead.
    2. The new state's Set actual end (WorkOrderSetActualEnd) is checked first. This event takes precedence: a state with Set actual end raises it even if the state also has Set actual start or is the lifecycle model's scheduled state, and the Started or Scheduled event is then not raised.
  • Which state changes count: every transition into a state with Set actual end, whatever process performs it. If your lifecycle model has more than one such state (for example an Ended and a Finished state), the event fires on each of those transitions. A later move into a state without the option (in the USMF demo data, Completed to Finished) raises nothing.
  • Timing: the standard lifecycle-state update saves the work order inside a transaction. send() writes the event to the business events commit log in that transaction and it is delivered after commit, so a state change that rolls back never delivers the event. The payload reflects the work order as saved in its new state, including the actual end the state has just stamped.
  • Enablement gate: guarded by BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeMaintenanceWorkOrderCompletedBusinessEvent)), so the event only sends when activated in the business events catalog.
  • No Lattice parameters, filters or batch jobs are involved; the event depends only on your lifecycle states.
// MGCLatticeEntAssetLifecycleStateUpdate_WorkOrderTable_Class_Extension
protected void updateStateRecord(RecId _recId)
{
RefRecId previousState = EntAssetWorkOrderTable::findRecId(_recId).WorkOrderLifecycleState;

next updateStateRecord(_recId);

MGCLatticeMaintenanceWorkOrderEventHandlers::raiseForLifecycleTransition(_recId, previousState);
}

// MGCLatticeMaintenanceWorkOrderEventHandlers::raiseForLifecycleTransition(_workOrderRecId, _previousState)
// reads the work order again, returns when the state is unchanged, and otherwise asks
// eventForTransition() which event (if any) the transition raises: Completed (Set actual end),
// Started (Set actual start) or Scheduled (the model's scheduled or mobile app scheduled state).

Payload Contract​

One row per [DataMember], in declaration order. The fields are declared on MGCLatticeMaintenanceWorkOrderBusinessEventContractBase, which the four maintenance work order events share, and are read in its initializeFromWorkOrder() from the updated work order header, the records it references and its work order lines.

JSON fieldDisplay labelType / EDTSourceNotes
legalEntityLegal EntityDataAreaIdEntAssetWorkOrderTable.DataAreaIdCompany of the work order, in lower case
workOrderIdWork OrderEntAssetWorkOrderIDEntAssetWorkOrderTable.WorkOrderIdWork order number
workOrderTypeWork Order TypeEntAssetWorkOrderTypeIDEntAssetWorkOrderType::findRecId(EntAssetWorkOrderTable.WorkOrderType).WorkOrderTypeIdThe work order type, for example Corrective or Preventive
descriptionDescriptionDescriptionEntAssetWorkOrderTable.DescriptionWork order description
lifecycleStateLifecycle StateEntAssetLifecycleStateIdEntAssetWorkOrderLifecycleState::find(EntAssetWorkOrderTable.WorkOrderLifecycleState).WorkOrderLifecycleStateIdID of the new lifecycle state, the one with Set actual end enabled
functionalLocationFunctional LocationEntAssetFunctionalLocationIDEntAssetFunctionalLocation::findRecID(EntAssetWorkOrderTable.FunctionalLocation).FunctionalLocationIdThe work order's functional location, which D365 sets from the lines when they all share one functional location (at the level set in Asset management parameters); empty otherwise
assetIdsAssetsList of EntAssetObjectIDEntAssetObjectTable.ObjectID of each EntAssetWorkOrderLine.Object, in line-number orderEvery distinct asset on the work order's lines, as a JSON array of strings; [] when the work order has no lines
responsibleWorkerPersonnelNumberResponsible Worker Personnel NumberHcmPersonnelNumberIdHcmWorker.PersonnelNumber of the responsible maintenance workerEmpty when no responsible worker is set
responsibleWorkerNameResponsible Worker NameHcmWorkerNameEntAssetWorker::find(EntAssetWorkOrderTable.ResponsibleWorker).workerName()Name of the worker responsible for the work order; empty when none
scheduledWorkerPersonnelNumberScheduled Worker Personnel NumberHcmPersonnelNumberIdHcmWorker.PersonnelNumber of the scheduled maintenance workerEmpty when no worker is scheduled
scheduledWorkerNameScheduled Worker NameHcmWorkerNameEntAssetWorker::find(EntAssetWorkOrderTable.WorkerScheduled).workerName()Name of the worker in the work order's "All maintenance jobs scheduled to" field; empty unless scheduling assigned every job to the same worker
expectedStartDateTimeExpected Start Date TimeutcdatetimeEntAssetWorkOrderTable.ExpectedStart"\/Date(ms)\/" form
expectedEndDateTimeExpected End Date TimeutcdatetimeEntAssetWorkOrderTable.ExpectedEndThe empty date-time (1900-01-01T00:00:00Z, "\/Date(-2208988800000)\/") when not set
scheduledStartDateTimeScheduled Start Date TimeutcdatetimeEntAssetWorkOrderTable.ScheduledStartPlanned start; the empty date-time when the work order was never scheduled
scheduledEndDateTimeScheduled End Date TimeutcdatetimeEntAssetWorkOrderTable.ScheduledEndPlanned end; the empty date-time when the work order was never scheduled
actualStartDateTimeActual Start Date TimeutcdatetimeEntAssetWorkOrderTable.ActualStartActual start, as stamped earlier by a Set actual start state; the empty date-time when the work order never passed through such a state (unless the completing state also has Set actual start)
actualEndDateTimeActual End Date TimeutcdatetimeEntAssetWorkOrderTable.ActualEndThe actual end the new state has just stamped

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 does not join a run: correlationId is unique to each emission.

Example Payload​

{
"BusinessEventId": "MGCLatticeMaintenanceWorkOrderCompletedBusinessEvent",
"ControlNumber": 588067,
"EventId": "7F20B9D4-3E61-4A8C-95B7-C4D18A2E6F03",
"EventTime": "\/Date(1790357400000)\/",
"EventTimeIso8601": "2026-09-25T17:30:00.8246845Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{744BA8C9-68B6-F0C2-461F-A65C8A28EE66}",
"ParentContextRecordSubjects": [],
"eventSequence": 48604,
"correlationId": "Event:e9a2c57b-1d43-4f08-b6e1-3a7f0c9d2b84",
"schemaVersion": 1,
"legalEntity": "usmf",
"workOrderId": "WO-000006",
"workOrderType": "Preventive",
"description": "Emergency Generator Weekly PM",
"lifecycleState": "Completed",
"functionalLocation": "PP-04-03",
"assetIds": ["GEN-002"],
"responsibleWorkerPersonnelNumber": "",
"responsibleWorkerName": "",
"scheduledWorkerPersonnelNumber": "000020",
"scheduledWorkerName": "Bill Malone",
"expectedStartDateTime": "\/Date(1790319600000)\/",
"expectedEndDateTime": "\/Date(-2208988800000)\/",
"scheduledStartDateTime": "\/Date(1790341200000)\/",
"scheduledEndDateTime": "\/Date(1790357400000)\/",
"actualStartDateTime": "\/Date(1790341200000)\/",
"actualEndDateTime": "\/Date(1790357400000)\/"
}

Activation​

In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Maintenance work order completed") or by filtering the Category column to Asset management. 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).

The event is raised only for lifecycle states that have Set actual end enabled; check this in Asset management > Setup > Work orders > Lifecycle states.