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 class | MGCLatticeWorkflowStalledOrFailedBusinessEvent |
| Contract class | MGCLatticeWorkflowStalledOrFailedBusinessEventContract |
| Category | System administration |
| Module | ModuleAxapta::SystemAdministration |
| Label IDs | WorkflowStalledOrFailedBusinessEventName / WorkflowStalledOrFailedBusinessEventDescription |
| Trigger | MGCLatticeWorkflowStalledAlertBatch.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(extendsRunBaseBatch), started from the action menu itemMGCLatticeWorkflowStalledAlertBatch("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
WorkflowTrackingStatusTablerow whoseTrackingStatusisFaultedorUnrecoverable, regardless of age (MGCLatticeWorkflowStalledOrFailedBusinessEvent::newFaulted()). - Stalled: every
WorkflowWorkItemTablerow whoseStatusisPendingorDelegatedand whosecreatedDateTimeis more than the threshold before the scan started (MGCLatticeWorkflowStalledOrFailedBusinessEvent::newStalled()).
- Faulted: every
- 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 ofrun(), thenMGCLatticeBusinessEventCompany::isPublishable(BusinessEventsFramework::Enabled()andBusinessEventsConfigurationReader::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 field | Display label | Type / EDT | Source | Notes |
|---|---|---|---|---|
alertReason | Alert Reason | str | Faulted: TrackingStatus (enum symbol). Stalled: constant | "Faulted", "Unrecoverable", or "Stalled" |
workflowCorrelationId | Workflow Correlation ID | str | CorrelationId of the status row or work item | The workflow instance's GUID, formatted with braces. For a work item inside a sub-workflow, the sub-workflow's |
workflowId | Workflow ID | str | Faulted: WorkflowTrackingStatusTable.ConfigurationNumber. Stalled: WorkflowTable.SequenceNumber of the work item's workflow | The workflow's ID as shown in the ID column of the workflows list, for example "000123" |
workflowConfiguration | Workflow Configuration | str | Faulted: WorkflowTrackingStatusTable.ConfigurationName. Stalled: WorkflowTable.Name | The workflow's name |
workflowType | Workflow Type | int | WorkflowTrackingStatusTable.WorkflowType (as integer) | 0 = Workflow, 1 = DependentSubworkflow, 2 = IndependentSubworkflow; 0 for a stalled work item whose instance status row can't be found |
workflowTypeName | Workflow Type Name | str | WorkflowTrackingStatusTable.WorkflowType (enum symbol) | "Workflow", "DependentSubworkflow", or "IndependentSubworkflow"; empty for a stalled work item whose instance status row can't be found |
workItemType | Work Item Type | str | Stalled: WorkflowWorkItemTable.Type (enum symbol) | For example "WorkItem", "RequestChange", "Return", "Escalated", "FinalApprover", "Recall"; empty for faulted instances |
legalEntity | Legal Entity | str | Faulted: WorkflowTrackingStatusTable.ContextCompanyId. Stalled: WorkflowWorkItemTable.CompanyId (the status row's ContextCompanyId if empty) | Lower case, for example "usmf" |
documentTableName | Document Table Name | str | Faulted: ContextTableId. Stalled: RefTableId (converted with tableId2Name) | Empty when no table is recorded |
documentRecId | Document Record ID | int64 | Faulted: ContextRecId. Stalled: RefRecId | |
currentStep | Current Step | str | Stalled: WorkflowElementTable.Name of the work item's step (its element name if the step has no name) | Empty for faulted instances |
currentStepId | Current Step ID | str | Stalled: WorkflowWorkItemTable.ElementId | The step's GUID, formatted with braces; unique where step names repeat. Empty for faulted instances |
assignedUser | Assigned User | str | Faulted: WorkflowTrackingStatusTable.Owner. Stalled: WorkflowWorkItemTable.UserId | Workflow owner for faulted instances; the work item's user for stalled items |
ageHours | Age Hours | int | Whole hours up to the scan | Faulted: since the instance's latest fault record (since the instance was created if there is none). Stalled: since the work item was created |
trackingStatus | Tracking Status | int | Faulted: WorkflowTrackingStatusTable.TrackingStatus (as integer) | 3 Faulted, 4 Unrecoverable; 0 for a stalled work item |
trackingStatusName | Tracking Status Name | str | Enum symbol of trackingStatus | Empty for a stalled work item |
workItemStatus | Work Item Status | int | Stalled: WorkflowWorkItemTable.Status (as integer) | 0 Pending, 1 Delegated; 0 for a faulted workflow |
workItemStatusName | Work Item Status Name | str | Enum symbol of workItemStatus | Empty for a faulted workflow |
detailText | Detail Text | str | Composed in the contract | Faulted: 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.