Skip to main content

Workflow stalled or failed

Fires from a scheduled scan for each workflow that needs administrator attention: a workflow instance that has faulted or become unrecoverable, or a work item that has been waiting for action longer than a threshold you set. "Stalled" is the absence of an action, which only a periodic scan can detect, so this event is raised by a batch job rather than by a live transition. Administrators can route it to Power Automate or Teams to chase stuck approvals before users notice them days later. Each condition is reported once; it's reported again only when it changes (for example, the work item is reassigned) or when it clears and later comes back.

Event classMGCLatticeWorkflowStalledOrFailedBusinessEvent
Contract classMGCLatticeWorkflowStalledOrFailedBusinessEventContract
CategorySystem administration
ModuleModuleAxapta::SystemAdministration
Label IDsWorkflowStalledOrFailedBusinessEventName / WorkflowStalledOrFailedBusinessEventDescription
TriggerMGCLatticeWorkflowStalledAlertBatch.run() -> one emission per faulted instance and per work item pending past the threshold that hasn't been reported in its current state

Trigger / Hook​

  • Batch class: MGCLatticeWorkflowStalledAlertBatch (extends RunBaseBatch), started from the action menu item MGCLatticeWorkflowStalledAlertBatch ("Workflow stalled/failed alert scan", under System administration > Periodic tasks > Lattice). Nothing else raises the event, so it fires only when this job runs.
  • Parameter: Stall threshold (hours), default 24. It's saved with the scheduled job.
  • run() first checks whether the event is activated in any legal entity. If not, it logs "The workflow stalled or failed business event is not activated in any legal entity; nothing to scan." and stops. Otherwise it runs two scans:
    • Faulted: every WorkflowTrackingStatusTable row whose TrackingStatus is Faulted or Unrecoverable, regardless of age (MGCLatticeWorkflowStalledOrFailedBusinessEvent::newFaulted()).
    • Stalled: every WorkflowWorkItemTable row whose Status is Pending or Delegated and whose createdDateTime is more than the threshold before the scan started (MGCLatticeWorkflowStalledOrFailedBusinessEvent::newStalled()).
  • Reported once: the scan remembers what it reported in the table MGCLatticeWorkflowAlertState, one row per open condition, keyed by the instance's correlation ID (faulted) or the work item's ID (stalled) and holding a fingerprint of the state it reported:
    • faulted instance: its status and its latest fault record, so a workflow that's resumed and faults again is reported again;
    • stalled work item: its status, assigned user and step, so a work item that's delegated, reassigned or moved to another step is reported again;
    • a condition that is still open and unchanged isn't sent again;
    • after both scans, conditions the scan no longer found open are forgotten, so a condition that clears and later comes back is reported again.
  • The memory is shared by every run of the scan. Schedule a single scan job: a second job with a different threshold forgets the first job's stalled items (it doesn't see them as open) and they're then reported again.
  • Both workflow tables are shared, so one run covers every company. Each condition is raised in its document's company (legalEntity; the scan's own company when the workflow names none or names a company that no longer exists): the activation check and the send run in that company (changecompany), so the event is published only where it is activated. A condition whose company hasn't activated it is neither sent nor remembered, and is reported by the first scan after activation.
  • The job runs outside a database transaction, so each send() is written to the business events commit log as it happens; a condition is remembered only after its event was sent.
  • Enablement gate: MGCLatticeBusinessEventCompany::isActivatedInAnyCompany(classStr(MGCLatticeWorkflowStalledOrFailedBusinessEvent)) at the start of run(), then MGCLatticeBusinessEventCompany::isPublishable (BusinessEventsFramework::Enabled() and BusinessEventsConfigurationReader::isBusinessEventEnabled) per condition, in its company.
private int scanStalled()
{
WorkflowWorkItemTable workItem;
utcdatetime cutoff = DateTimeUtil::addHours(scanStartDateTime, -thresholdHours);
int alertCount;

while select workItem
where (workItem.Status == WorkflowWorkItemStatus::Pending
|| workItem.Status == WorkflowWorkItemStatus::Delegated)
&& workItem.createdDateTime < cutoff
{
if (this.reportStalled(workItem))
{
alertCount++;
}
}

return alertCount;
}

protected boolean reportStalled(WorkflowWorkItemTable _workItem)
{
boolean reported;

changecompany(MGCLatticeBusinessEventCompany::resolve(MGCLatticeWorkflowStalledOrFailedBusinessEventContract::workItemCompany(_workItem)))
{
if (this.isEventPublishable())
{
Description255 alertState = MGCLatticeWorkflowStalledOrFailedBusinessEvent::stalledAlertState(_workItem);

if (MGCLatticeWorkflowAlertState::shouldReport(
MGCLatticeWorkflowStalledOrFailedBusinessEvent::AlertKindStalled, _workItem.Id, alertState, scanStartDateTime))
{
this.sendAlert(MGCLatticeWorkflowStalledOrFailedBusinessEvent::newStalled(_workItem));

MGCLatticeWorkflowAlertState::recordReported(
MGCLatticeWorkflowStalledOrFailedBusinessEvent::AlertKindStalled, _workItem.Id, alertState, scanStartDateTime);

reported = true;
}
}
}

return reported;
}

Payload Contract​

One row per [DataMember], in declaration order. The contract is built by the scan: initializeFromFaulted() reads a WorkflowTrackingStatusTable row (and its latest fault record in WorkflowTrackingTable); initializeFromStalled() reads a WorkflowWorkItemTable row, its workflow instance's WorkflowTrackingStatusTable row, its workflow (WorkflowVersionTable / WorkflowTable) and its step (WorkflowElementTable).

JSON fieldDisplay labelType / EDTSourceNotes
alertReasonAlert ReasonstrFaulted: TrackingStatus (enum symbol). Stalled: constant"Faulted", "Unrecoverable", or "Stalled"
workflowCorrelationIdWorkflow Correlation IDstrCorrelationId of the status row or work itemThe workflow instance's GUID, formatted with braces. For a work item inside a sub-workflow, the sub-workflow's
workflowIdWorkflow IDstrFaulted: WorkflowTrackingStatusTable.ConfigurationNumber. Stalled: WorkflowTable.SequenceNumber of the work item's workflowThe workflow's ID as shown in the ID column of the workflows list, for example "000123"
workflowConfigurationWorkflow ConfigurationstrFaulted: WorkflowTrackingStatusTable.ConfigurationName. Stalled: WorkflowTable.NameThe workflow's name
workflowTypeWorkflow TypeintWorkflowTrackingStatusTable.WorkflowType (as integer)0 = Workflow, 1 = DependentSubworkflow, 2 = IndependentSubworkflow; 0 for a stalled work item whose instance status row can't be found
workflowTypeNameWorkflow Type NamestrWorkflowTrackingStatusTable.WorkflowType (enum symbol)"Workflow", "DependentSubworkflow", or "IndependentSubworkflow"; empty for a stalled work item whose instance status row can't be found
workItemTypeWork Item TypestrStalled: WorkflowWorkItemTable.Type (enum symbol)For example "WorkItem", "RequestChange", "Return", "Escalated", "FinalApprover", "Recall"; empty for faulted instances
legalEntityLegal EntitystrFaulted: WorkflowTrackingStatusTable.ContextCompanyId. Stalled: WorkflowWorkItemTable.CompanyId (the status row's ContextCompanyId if empty)Lower case, for example "usmf"
documentTableNameDocument Table NamestrFaulted: ContextTableId. Stalled: RefTableId (converted with tableId2Name)Empty when no table is recorded
documentRecIdDocument Record IDint64Faulted: ContextRecId. Stalled: RefRecId
currentStepCurrent StepstrStalled: WorkflowElementTable.Name of the work item's step (its element name if the step has no name)Empty for faulted instances
currentStepIdCurrent Step IDstrStalled: WorkflowWorkItemTable.ElementIdThe step's GUID, formatted with braces; unique where step names repeat. Empty for faulted instances
assignedUserAssigned UserstrFaulted: WorkflowTrackingStatusTable.Owner. Stalled: WorkflowWorkItemTable.UserIdWorkflow owner for faulted instances; the work item's user for stalled items
ageHoursAge HoursintWhole hours up to the scanFaulted: since the instance's latest fault record (since the instance was created if there is none). Stalled: since the work item was created
trackingStatusTracking StatusintFaulted: WorkflowTrackingStatusTable.TrackingStatus (as integer)3 Faulted, 4 Unrecoverable; 0 for a stalled work item
trackingStatusNameTracking Status NamestrEnum symbol of trackingStatusEmpty for a stalled work item
workItemStatusWork Item StatusintStalled: WorkflowWorkItemTable.Status (as integer)0 Pending, 1 Delegated; 0 for a faulted workflow
workItemStatusNameWork Item Status NamestrEnum symbol of workItemStatusEmpty for a faulted workflow
detailTextDetail TextstrComposed in the contractFaulted: Workflow <ID> '<name>' is <status> on <table> record <RecId> in company <company>. Stalled: Work item on step '<step>' of workflow <ID> '<name>' assigned to '<user>' has been <status> for <hours> hours (<table> record <RecId> in company <company>).

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. The workflow instance's own correlation ID is in workflowCorrelationId; to recognize a repeat report of the same condition, use workflowCorrelationId with alertReason, plus currentStepId and assignedUser for a stalled work item. BusinessEventLegalEntity is the document's company (company; the scan's own company when the workflow names none).

Example Payload​

A purchase requisition approval that has been pending for 68 hours, reported by a scan with the default 24-hour threshold:

{
"BusinessEventId": "MGCLatticeWorkflowStalledOrFailedBusinessEvent",
"ControlNumber": 5637151455,
"EventId": "F1D8B270-6C3E-4A95-B047-2E9A5C8D1F36",
"EventTime": "\/Date(1790316003000)\/",
"EventTimeIso8601": "2026-09-25T06:00:03.7592840Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{54AB148F-34AA-5C3F-A78F-7394F1C75AC0}",
"ParentContextRecordSubjects": [],
"eventSequence": 49017,
"correlationId": "Event:2c81f4a6-93d7-4e5b-a0c8-6f1b3d9e7a24",
"schemaVersion": 1,
"alertReason": "Stalled",
"workflowCorrelationId": "6b3d9e1a-0f42-4c87-a5d3-7e28b1c94f60",
"workflowId": "000123",
"workflowConfiguration": "Purchase requisition review",
"workflowType": 0,
"workflowTypeName": "Workflow",
"workItemType": "WorkItem",
"legalEntity": "usmf",
"documentTableName": "PurchReqTable",
"documentRecId": 5637145110,
"currentStep": "Approve purchase requisition",
"currentStepId": "a17c3e05-58b2-4d9f-8e64-0b92f7d1c3a8",
"assignedUser": "Admin",
"ageHours": 68,
"trackingStatus": 0,
"trackingStatusName": "",
"workItemStatus": 0,
"workItemStatusName": "Pending",
"detailText": "Work item on step 'Approve purchase requisition' of workflow 000123 'Purchase requisition review' assigned to 'Admin' has been pending for 68 hours (PurchReqTable record 5637145110 in company usmf)."
}

Activation​

In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Workflow stalled or failed") 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).

Then schedule the scan, or nothing is ever raised: System administration > Periodic tasks > Lattice > Workflow stalled/failed alert scan (menu item MGCLatticeWorkflowStalledAlertBatch). It's granted by the privilege "Run the workflow stalled or failed alert" (MGCLatticeWorkflowStalledAlertBatchExecute) in the Integration administrator role, and to system administrators. In the dialog, set Stall threshold (hours), then under Run in the background turn on Batch processing and set a recurrence - for example hourly. Because each condition is reported only once, the recurrence decides how quickly a new fault or stalled work item is reported, not how often you're reminded. Schedule only one scan job.