Skip to main content

MGCLattice - Offerings Deep Dive

Magicite · Microsoft Dynamics 365 Finance & Operations

This is the analytical companion to the lighter one-page product view, Product offerings. Where that page enumerates, this document explains the shape of the offering - what each capability is for, how the pieces compose, the design decisions behind them, and how a customer adopts them. Field-level payload detail for individual events lives in Event catalog.


1. Executive summary​

D365 F&O ships a business-events framework and a standard business events catalog. The catalog looks long (the reference list Lattice embeds for its coverage report, taken from a Finance and Operations environment, has 211 entries), but most of its entries are workflow events, generated for each configured workflow, and Human resources, batch, alert and background operation events; the exact list depends on the version and on the workflows configured. The finance and supply chain events are comparatively few, and most of them report a posted document or a warehouse integration message. Most process-level moments a real integration cares about - "the production order was reported as finished", "the transfer order shipped", "the vendor check was issued", "on-hand changed" - are not emitted by the platform. Teams then hand-build one-off events per project, usually hooked on the wrong seam (a table write), with no audit trail, no replay, and no way to know what they have.

MGCLattice is two products in one model:

  1. An event catalog - 81 process-level business events across fourteen business domains, each hooked, wherever one exists, on the process method that performs the business action (not the table write), each with an enriched, integration-ready payload, each activatable with zero code through the standard Business Events Catalog.

  2. An integration-reliability & operations platform - four cross-cutting capabilities that turn "we fire events" into "we can prove, recover, inventory, and bootstrap our event estate": audit + replay, reconciliation, catalog discovery + coverage, and bulk initial-load / resync.

The catalog answers "what happened?". The platform answers "can I trust it, see it, and recover it?" - which is what separates a bag of custom events from a supportable integration product.

DimensionNative D365 F&OMGCLattice
Process-level event coverage~sparse, notification-leaning81 events, 16 modules
Trigger altituden/a (you build it)process method, post-commit-safe
Payloadvariesenriched: human-readable enums, resolved names, amounts, keys
Outbound audit trailnoneevery opt-in publish logged with outcome
Replay of a failed/lost publishnoneper-event replay from the audit log
Automatic recoverynonehourly reconciliation background process
Catalog introspectionmanualdiscovery → explorer + coverage report
Initial load / DR resynchand-rolled per projectgeneric, throttled, registration-driven engine

2. The offering model​

Think of the offering as two layers with a thin shared spine.

┌─────────────────────────────────────────────────────────────┐
LAYER A │ EVENT CATALOG - 81 process-level events, 14 domains │
(what │ Vendor · Customer · Production · Sales · Purchasing · │
happened) │ Inventory · Warehouse · Finance · Project · Planning · │
│ Retail/Product · Pricing · Asset maintenance · Platform │
└─────────────────────────────────────────────────────────────┘
│ every opt-in event flows through
▼ the single publish chokepoint
┌─────────────────────────────────────────────────────────────┐
SPINE │ BusinessEventsPublisher (platform) ◄── CoC wrapper │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────┬─────────┴────────┬──────────────────┬─────────────────┐
LAYER B │ Audit+Replay │ Reconciliation │ Catalog Discovery │ Bulk Load/Resync│
(can I trust, │ │ │ + Coverage │ │
see, recover)│ │ │ │ │
└───────────────┴──────────────────┴───────────────────┴─────────────────┘

The spine is the key architectural bet: because every loggable event is published through one platform method (BusinessEventsPublisher.publish), a single chain-of-command wrapper gives any event that opts in audit, replay, and reconciliation without per-event wiring. Add a new event tomorrow; if it opts into the marker interface, it is audited and reconcilable for free.


3. Layer A - the event catalog​

All 81 events follow the same three-part anatomy (event class extending BusinessEventsBase, a [DataContract] contract, and a trigger - usually a Chain-of-Command wrapper; some use a handler on a standard delegate or data event, and a few are raised by a Lattice background process or API). Two conventions make the payloads integration-friendly across the board:

  • Enums ship twice - an integer (enum2int) and a human-readable string (enum2Symbol), e.g. productionStatus
    • productionStatusName. Consumers never have to maintain an enum lookup table.
  • Keys over RecIds - payloads carry business identifiers (item number, sales id, account) and resolved names, not internal record ids, so downstream systems can act without calling back into D365.

The trigger column below reflects the process-method standard the catalog follows (see §5.1). Field-level payloads are in Event catalog.

3.1 Vendor (6)​

EventBusiness momentTrigger (process hook)Payload highlights
Vendor CreatedA vendor master record is createdVendTable.insert() (master-data exception)account, name, group, currency
Vendor UpdatedA vendor master record changesVendTable.update() (exception)account, name, group, currency
Vendor Hold Status ChangedA vendor's hold is applied, changed or released, or a vendor is created on holdVendTable Updated data event → Blocked changed; VendTable.insert() → new vendor on holdaccount, name, new and previous hold status (+names), new-vendor flag
Vendor Check IssuedA check is issued to a vendorBankChequeTable.update() → ChequeStatus Created/NotPrinted/Testprint → Payment, RecipientType==Vendbank account, check number, payee name and account, amount, currency, date, voucher
Vendor Payment PostedA vendor payment journal postsLedgerJournalCheckPost.runInternalPostJournalTransactionEnded (post-commit delegate) → posted Payment journal, one event per vendor postingjournal, voucher, vendor, amount signed from the vendor's side, debit/credit, currency, payment method, reference, date
Vendor ASN Line ChangedA purchase ASN line is created, or its quantity changesWHSASNItem.insert() / update() → purchase ASN line (Module == Purch)PO, shipment, load, item, quantity (+previous), unit, batch expiry

Why it matters: vendor master events drive supplier-portal / P2P master-data replication; the check event feeds positive-pay files to banks and treasury fraud controls, and notably exposes the physical check number and payee, which the native ledger-posting event never surfaces.

3.2 Customer (7)​

EventBusiness momentTriggerPayload highlights
Customer CreatedCustomer master createdCustTable.insert() (exception)account, name, group
Customer UpdatedCustomer master changesCustTable.update() (exception)account, name, group
Customer Hold Status ChangedBlocked/credit status changesCustTable.update() field-deltaaccount, name, blocked status (+name), credit limit
Customer Credit Limit ExceededThe standard credit limit check failsCustCreditLimit.check() returns falseaccount, name, the credit limit in effect for the check, balance, order balance and total exposure (accounting currency), triggering context
Collection Case ChangedA collections case is opened or closedCustCollectionsCaseDetail::createCase() / closeCase()customer, case id, status (+name), action, balance, event date (the user's current date)
Collection Letter PostedA collection letter is postedCustCollectionLetterPost.writeCustCollectionLetterJour() → letter status Postedcustomer, letter number, level (+name), letter and due dates, fee, currency, voucher, posting date
Dunning Level ChangedA letter changes an open transaction's collection letter levelCustCollectionLetterJour.updateCollectionLetterCodeCustTrans()customer, letter, new and previous level (+names), invoice, remaining amount, currency, voucher, dates

Why it matters: CRM/CDP master sync and real-time credit-risk workflows. Customer Hold Status Changed is a field-delta event by nature (it compares orig().Blocked), which is exactly why it correctly remains a buffer-level hook (see §5.1).

3.3 Production (9)​

EventBusiness momentTriggerPayload highlights
Production Order ScheduledStatus → ScheduledProdUpdScheduling.run(); ReqTransPoMarkFirm.insertMovement() (planned order firmed as Scheduled)order, item (+name), status (+name)
Production Order EstimatedStatus → CostEstimatedProdUpdCostEstimation.run()order, item
Production Order StartedStatus → StartedUpProdUpdStartUp.run()order, item (+name), status (+name)
Production Pick List PostedPicklist journal postsProdJournalCheckPostBOM.run()order, journal, item, status (+name), posted component lines (item, quantity, unit, operation)
Production Route Card PostedRoute-card journal postsProdJournalCheckPostRoute.run()order, journal, item, status (+name), posted lines (operation, resource, hours, good/error qty)
Production Order Reported as FinishedStatus → ReportedFinishedProdUpdReportFinished.run(); ProdJournalCheckPostProd.run() (report-as-finished journal)order, item, status (+name), total good/error qty reported finished, unit
Production Order EndedStatus → CompletedProdUpdHistoricalCost.run()order, item, status (+name), total good/error qty reported finished, unit
BOM Calculation CompletedA cost calc row is producedBomCalcItem.insertBOMCalcTable()item, cost price, calc date
BOM Version ApprovedA BOM/formula version is approvedBomVersionApprove.run() (Approved No→Yes)item, BOM id (+name), approver (+name), validity

Why it matters: the full production lifecycle for MES hand-off, shop-floor dashboards, and finite-scheduling integration. The pick-list/route-card events capture actual posted quantities at the posting seam - and the BOM events split the engineering-of-record (version approval) from costing (BOM calc), which downstream PLM/MRP care about separately.

3.4 Sales (10)​

EventBusiness momentTriggerPayload highlights
Sales Order ConfirmedConfirmation journal posts (not pro forma)SalesConfirmJournalPost.postJournalPost()order, customer, confirm id, currency, total
Sales Packing Slip PostedPacking slip posts (excl. pro forma, cancellations and return order receipts)SalesPackingSlipJournalPost.postJournalPost()order, customer, packing slip, delivery date
Sales Order Invoice PostedA sales order invoice or credit note posts (not pro forma)SalesInvoiceJournalPost.postJournalPost()invoice account, order, invoice, date, amount, currency
Sales Return Order CreatedA return order is createdSalesTableType_ReturnItem.inserted()return order, customer, reason
Return Order ReceivedA return order line is registered (goods arrived)SalesLine.returnLineRegisterUpdate() / SalesReturnNonStockedRegistrationHelper.updateOnRegistration() → ReturnStatus → RegisteredRMA, order, item, quantity, disposition code, warehouse
Return Credit Note PostedA return order is invoiced (its credit note posts)SalesInvoiceJournalPost.postJournalPost() → return order (SalesType ReturnItem)credit note, return order, RMA, customer, amount, currency
Sales Quotation ConfirmedA quotation is confirmedSalesQuotationEditLinesForm_Sales_Confir.createJournal()quotation, confirmation, sales order created, customer, confirm date
Sales Order CanceledStatus → CanceledSalesUpdateRemain.cancelOpenOrderLinesDeliveryRemainder() / cancelMultipleOrdersDeliveryRemainder() / MCRSalesOrderCancellation::cancelOrder() / cancelOrderFTC()order, customer, status (+name), sales type (+name), RMA number
Sales Order Line ChangedA line's quantity, price or requested dates changeSalesLine Updated data eventorder, item, line, previous/current quantity, price, receipt and ship dates, currency
Sales Agreement ConfirmedA sales agreement snapshot is takenAgreementConfirm_Sales.createHeaderHistory()agreement, confirmation number, customer, effective/expiration

Why it matters: order-to-cash visibility for e-commerce, customer notifications (ASN precursors), and contract management. Confirmation, packing slip and invoice hook the FormLetter postJournalPost() seam, so they fire once per journal - semantically exact and impossible to miss via set-based posting.

3.5 Purchasing (3)​

EventBusiness momentTriggerPayload highlights
Purchase Return Order CreatedA purchase return is createdPurchTableType_ReturnItem.inserted()return order, vendor, reason
Purchase Requisition ApprovedA requisition reaches ApprovedPurchReqWFStatusTransitionHelper::setPurchReqTable2Approved() (workflow)requisition, name, requester, buying legal entities
Purchase Agreement ConfirmedA purchase agreement snapshotAgreementConfirm_Purch.createHeaderHistory()agreement, vendor, effective/expiration

Why it matters: procure-to-pay automation and supplier collaboration. PurchReqApproved hooks the workflow transition helper (guarded against reset-to-approved replays), so it reflects the genuine approval, not every row save.

3.6 Inventory (10)​

EventBusiness momentTriggerPayload highlights
Transfer Order ShippedTransfer status → ShippedInventTransferUpdShip.run()transfer, from/to warehouse, status (+name)
Transfer Order ReceivedTransfer status → ReceivedInventTransferUpdReceive.run()transfer, from/to warehouse, status (+name)
Transfer Order Picking List CreatedGenerate picking list creates a transfer picking listInventTransferUpdPick.run()route, order, trans type (+name), route status, lines
Inventory Closing CompletedAn inventory close or recalculation finishesInventCostLedgerPostingTask.updateLedgerPosting()closing date, voucher, close or recalculation
Quality Order PassedQuality order → PassInventQualityOrderValidate.run()quality order, item, test group, status (+name)
Quality Order FailedQuality order → FailInventQualityOrderValidate.run() (shared)+ disposition code, blocking qty, worker, route
Released Product CreatedA product is released to a legal entityEcoResProductReleaseManager.createInventTable() / EcoResProductReleaseManagerItem.release()item, product RecId, search name, type (+name), lifecycle state
Inventory On-Hand ChangedPhysical on-hand changesInventUpdateOnhand.onOnHandUpdated (delegate); sent after the transaction commitsitem, site, warehouse, physical change, on-hand quantities, unit
Item Back In StockAvailable physical moves from 0 or less to above 0InventUpdateOnhand.onOnHandUpdated (delegate); sent after the transaction commitsitem, site, warehouse, previous/current available, threshold, unit
Picking List CanceledA picking list or a single picking line is canceledWMSPickingRoute.cancel() (CoC) + post-handler on WMSOrderTransType_Output_OrderPick.cancelroute, order, trans type (+name), canceled lines

Why it matters: the deepest domain - covers WMS/3PL integration, quality (pharma/food/regulated verticals), the item-master onboarding moment (Released Product), and a throttled on-hand feed for marketplaces/e-commerce. Quality Order Passed and Quality Order Failed deliberately share one process wrapper (InventQualityOrderValidate.run()) with mutually exclusive Pass/Fail branches.

3.7 Warehouse (3)​

EventBusiness momentTriggerPayload highlights
Warehouse Load ShippedA load is ship-confirmed, on its own or through its transport loadWHSShipConfirm.shipConfirm(WHSLoadId); WHSShipConfirmationActionTransportLoad.shipConfirm()load, shipment, carrier, status (+name)
Warehouse Work CompletedWHS work closesWHSWorkTable.update() → Closed; WHSWorkTable.insert() of work created closed (documented exception)work id, status and type (+names), load/shipment/order, site/warehouse, close time
Warehouse Picking CompletedPicking work closesWHSWorkTable.update() → Closed, work with a closed Pick line (documented exception)work id, type (+name), order/shipment/load, site/warehouse, pick line count, per-line item, quantity, unit, location, picker

Why it matters: advanced-warehouse signals for ASN generation, carrier integration, and labor/throughput analytics. Work-completed is a deliberate documented exception to the process-hook standard - there is no single safe process chokepoint (9+ scattered closing paths, some born-closed inserts), so the table hook with a tight closed-transition guard is the correct altitude (see §5.1).

3.8 Finance (7)​

EventBusiness momentTriggerPayload highlights
General Journal PostedA daily journal postsLedgerJournalCheckPost.runInternalPostJournalTransactionEnded (post-commit delegate)journal, name, posting date
Free Text Invoice PostedAn FTI posts (not pro forma)CustInvoiceJour.insert() (posting-only exception)invoice, customer, date, amount, currency
Fixed Asset Acquisition PostedAcquisition transaction postsAssetTrans.insert() → Acquisition (exception)asset, book, amount, date, voucher, reversal/reclassification flags
Fixed Asset Disposal PostedDisposal transaction postsAssetTrans.insert() → DisposalSale/Scrap (exception)asset, book, amount, date, voucher, reversal/reclassification flags
Bank Reconciliation CompletedA bank statement is reconciled (classic Reconcile account)BankAccountReconcile.updateBankAccountStatementToReconciled() → ReconcileDate first setbank account, statement number and date, currency, ending balance, reconcile date, matched count and amount
Advanced Bank Reconciliation CompletedAn Advanced bank reconciliation worksheet is marked as reconciledBankReconciliationHeader::reconcile() / reconcileInBatch() → IsReconciled No → Yesreconciliation, bank account, cut-off and reconciled dates, statements covered, bank currency, matched statement-line and bank-transaction counts and bank-currency totals, reconciled by
E-invoice Submission Status ChangedAn Electronic invoicing submission is logged or changes statusBusinessDocumentSubmission Inserted, or Updated when State changesdocument reference, source table and record, status (+name), response detail, UTC submission time

Why it matters: finance-close automation, sub-ledger reconciliation, and fixed-asset lifecycle. General Journal Posted uses the platform's post-commit delegate - the cleanest possible "it actually committed" signal. Free Text Invoice Posted and the two fixed asset events sit on posting-only ledger tables (written exclusively by posting engines, with no entity/import path), so the table hook is genuinely at-posting and correctly kept as an exception.

3.9 Project (2)​

EventBusiness momentTriggerPayload highlights
Project Invoice PostedA project invoice posts (not pro forma)ProjInvoiceJour.insert() (posting-only exception)project, invoice, invoice account, amount, currency, date
Project Task AssignedA resource is assigned to a project task (work breakdown structure), or an assignment row is written by a customization or importProjTask::reserveResourceActivity(); ProjActivityAssignment Inserted/Updated data eventsproject, activity, resource, worker, assigned hours, load %, capacity-limited flag, action

3.10 Planning (1)​

EventBusiness momentTriggerPayload highlights
MRP Run CompletedA master-planning run finishes (a canceled run raises nothing)ReqCalc.run() (main thread of a ReqCalcScheduleItemTable run)plan id

3.11 Retail & Product catalog (3)​

EventBusiness momentTriggerPayload highlights
Retail Assortment PublishedAn assortment is published for the first time in a legal entity, or its effective item list changesRetailAssortmentLookupTask.run()assortment, customer, per-item price-at-publish, item list
External Item ChangedA cust/vend external item cross-ref changesCustVendExternalItem Inserted/Updated data eventsitem, external id, module type (+name), relation, dim
Retail Info-code Response CapturedAn info code (reason code) response is recorded in headquartersRetailTransactionInfocodeTrans.insert()code, subcode, text, amount, transaction and line, store, terminal, staff, UTC time

Why it matters: commerce/catalog distribution. Retail Assortment Published carries the customer-specific price computed at publish time (the reusable "price-at-publish" technique) - when an assortment is tied to a customer, each line's price is the resolved trade-agreement price for the current date in the user's time zone, not the base price.

3.12 Pricing & trade agreements (7)​

EventBusiness momentTriggerPayload highlights
Sales Price Trade Agreement ChangedAny sales-price agreement row is inserted or updatedPriceDiscTable Inserted/Updated data events (relation PriceSales)account and item relation, currency, amount, price unit, quantity from, from/to dates, unit, inventory dimensions, record id, update flag
Sales Price Agreement CreatedA posted journal line adds a price with none in effect on its From datePriceDiscAdmCheckPost.createOrUpdatePriceDiscTableRecords()shared price payload: change type (+name), account and item relation, currency, unit, quantity break, dimensions, dates, amount, record id
Sales Price Agreement ModifiedA posted journal line changes an existing pricePriceDiscAdmCheckPost.createOrUpdatePriceDiscTableRecords()shared price payload + old amount, changed fields
Sales Price Agreement SupersededA posted journal line adds a price where one was in effect on its From datePriceDiscAdmCheckPost.createOrUpdatePriceDiscTableRecords()shared price payload + the superseded price's amount and record id
Sales Price Agreement DeletedA price is deleted by a user, by code or by a posted journalPriceDiscTable.delete() / PriceDiscAdmTable::deleteMarked()shared price payload
Sales Price Agreement EffectiveA price's From date is reachedMGCLatticeTradeAgreementPriceEffectivityBatch.run() (daily background process)shared price payload
Sales Price Agreement ExpiredA price's To date passes with no other price of the agreement in effectMGCLatticeTradeAgreementPriceEffectivityBatch.run() (daily background process)shared price payload

3.13 Asset maintenance (4)​

EventBusiness momentTriggerPayload highlights
Maintenance Work Order CreatedA work order is createdEntAssetWorkOrderTable Inserted data event, raised when the creating transaction commitswork order, type, description, lifecycle state, functional location, assets, workers, expected/scheduled/actual dates
Maintenance Work Order ScheduledIt moves to the lifecycle model's scheduled stateEntAssetLifecycleStateUpdate_WorkOrderTable.updateStateRecord()as Created
Maintenance Work Order StartedIt enters a state that sets the actual startEntAssetLifecycleStateUpdate_WorkOrderTable.updateStateRecord()as Created
Maintenance Work Order CompletedIt enters a state that sets the actual endEntAssetLifecycleStateUpdate_WorkOrderTable.updateStateRecord()as Created

3.14 Platform & integration (9)​

EventBusiness momentTriggerPayload highlights
Batch Job Status ChangedA batch job's status changes through the job record, unless a batch job event filter excludes the jobBatchJob.update() (+ batch job event filter)job, caption, company, status and previous status (+names), start/end, task count
DMF Execution CompletedA data project entity execution reaches Finished, Error or CanceledDMFDefinitionGroupExecution.update() / DMFEntityDbExporter::generateEntityDBDataV2()execution, project, entity, outcome, target/staging status (+names), record and error counts
Recurring Data Job Health ChangedThe health of a recurring data job with alerts on changes (for example Healthy to Failing, or Broken back to Healthy)MGCLatticeRecurringJobHealthAlerts background process (every 15 minutes) and health refreshes on the recurring integration pagesjob, activity ID, direction, data project, new and previous health (+names), reasons, backlog, failed and stuck message counts, processing and monitoring batch jobs
Aggregate Measurement Refresh CompletedA full-reset entity store refresh finishesBIMeasurementProcessingStatisticsFull.update()measurement, refresh type, status (+name), succeeded, start/completion, duration
Workflow Lifecycle ActionA reportable workflow action is recordedWorkflowTrackingCommentTable.insert()action, tracking type (+name), workflow, document, step, acting user, assigned-to user, comment, UTC time
Workflow Stalled or FailedA workflow faults, or a work item waits past the thresholdMGCLatticeWorkflowStalledAlertBatch.run() (hourly background process)alert reason, workflow, document, step, assigned user, age, status
Integration Item ParkedA failed Lattice async operation or file ingestion item is parked in the dead-letter queueMGCLatticeIntegrationDlqAlert.onParked()DLQ id, source, service and operation, error code, status, attempts, first failure time, message
Business Event Publish FailedPublishing an opted-in Lattice event fails outside a transactionBusinessEventsPublisher.publish() (Lattice wrapper, see §4.1)failed event id and class, direction, identifier, failure details, log RecId, UTC time
Custom Ad-hoc Business EventCustomer X++ code raises itMGCLatticeCustomAdHocEventApi::raiseEvent()event name, table and record reference, key fields, JSON payload

4. Layer B - the integration-reliability & operations platform​

This is what makes the catalog operable. Four capabilities, all leaning on the single publish chokepoint.

4.1 Audit & Replay​

Problem solved. The platform keeps no record of the business events it publishes: a failed or lost publish leaves no trace of what was lost and nothing to resend.

How it works.

  • IMGCLatticeBusinessEventLoggable - a marker interface an event implements (plus getEventIdentifier / getEventIdentifierType) to opt in. No other wiring.
  • MGCLatticeBusinessEventsPublisher_Class_Extension - a CoC wrapper on BusinessEventsPublisher.publish that, in a finally block, writes a MGCLatticeBusinessEventLog row for an opt-in publish: Success with the serialized payload when the event reached the commit log, Error when the publish failed outside a database transaction. That failure is kept from the caller and becomes a durable Error row - no longer silent - and raises the Business Event Publish Failed event. Inside a transaction X++ runs no catch, so a failure propagates and rolls back the caller's transaction, and nothing is logged. Events that don't opt in pass straight through, failures included.
  • Two-tier opt-in: implementing the interface = audited; additionally exposing a static newFromLog(MGCLatticeBusinessEventLog) factory = replayable. MGCLatticeBusinessEventLogHelper.replayFromLog rebuilds the event from the stored contract and re-sends it; audit-only events surface a clear "no replay factory" message. In this release Retail Assortment Published is audited and replayable, and Business Event Publish Failed is audited only.

Why the design is sound. BusinessEventsBase.send() writes to the commit log inside the transaction with async post-commit delivery, so a Success row written inside the caller's transaction commits or rolls back together with the event it records. The logging path is guarded rather than exception-handled: it runs only for opt-in events, never for a failure inside a transaction (the row would be rolled back with it), and for a success only when the event was actually written to the commit log.

4.2 Reconciliation sweep​

Problem solved. The dead-letter retry process only catches loud failures (caught exceptions). The swallowed-publish class leaves no DLQ entry, but audit (§4.1) turns those into Error rows, and the reconciliation sweep re-drives them.

How it works.

  • MGCLatticeBusinessEventLog is extended with ReconcileState (NotReconciled / Resolved / Exhausted / ManualRequired), RetryCount, LastRetryDateTime.
  • MGCLatticeBusinessEventReconciliationSweep runs by itself: the Lattice business event reconciliation background process (System administration > Setup > Process automations > Background processes) runs it hourly, so there is nothing to schedule, and a batch job is created only when there are failed events to re-publish. It collects eligible Error rows - older than a min-age window so in-flight failures are left alone, under the retry ceiling - then re-publishes each in its own transaction via replayFromLog (idempotent by original key). Outcomes:
    • Resolved - re-published (a fresh audit row records the new attempt's outcome).
    • Exhausted - hit max retries → a loud trace at last, the DLQ-equivalent that was missing.
    • ManualRequired - audit-only event with no replay factory (detected up front via hasStaticMethod).
  • Defaults: 5 retries, 15-minute min age. The background process always uses these defaults; a run started from the Business event reconciliation sweep menu item can use other values.
  • One sweep job covers every company: each Error row is replayed and updated in the company it was logged in, so the replayed event is published as that company's event.

Scope (by design). This covers the logged-with-Error class. Two related classes are intentionally out of scope: never-acknowledged (the log records publish outcome, not downstream consumer ACK - needs an ack-ingestion mechanism) and expected-but-absent / never-emitted (needs per-event expectation providers; the source-side fix is transactional idempotency).

4.3 Catalog discovery & coverage​

Problem solved. Teams could not answer "what business events do we actually have, and how do we compare to a reference list?"

How it works.

  • MGCLatticeBusinessEventCatalogDiscovery.refresh() enumerates every BusinessEventsBase carrying the [BusinessEvents] attribute via the supported DictClass::getAttributedClasses(...) - the same path the platform's own BusinessEventsCatalogHelper uses (no internal/metadata-provider access). It populates MGCLatticeBusinessEventCatalog (event inventory: category, contract, payload-field count) and MGCLatticeBusinessEventCatalogMember (the payload schema, one row per [DataMember]/[BusinessEventsDataMember]).
  • MGCLatticeBusinessEventCatalogExplorer - a master-detail form to browse the catalog and each event's payload.
  • Coverage validation: MGCLatticeBusinessEventReference holds an external reference list (a 211-entry D365 F&O business-events catalog is embedded, deployable with no runtime file dependency), and coverageReport() reports both gap directions - reference events absent from this environment, and discovered events not in the reference. A one-click MGCLatticeBusinessEventCoverageReport refreshes, seeds-if-empty, and reports.

4.4 Bulk initial-load / resync​

Problem solved. Standing up a new downstream system, or recovering one, needs a full replay of current master data as events - not just go-forward deltas.

How it works.

  • IMGCLatticeBulkEventSource + MGCLatticeBulkEventPublisher - a generic, registration-driven engine (one switch entry per entity, not one class per entity) that walks an entity and emits its event per record, with throttling (configurable batch size + pause) and progress reporting.
  • Sources: Customer → CustomerCreated, Item → ReleasedProductCreated, ExternalItem → ExternalItemChanged.
  • Drivers + menu items (MGCLatticeProcessAllCustomers/Items/ExternalItems) run under SysOperationSandbox.
  • A run works in the current legal entity and, before reading any record, checks that the source's event is published there (business events enabled, event activated); otherwise it stops with an error and publishes nothing. The finish message counts only the events actually published.
  • Composes with audit and replay automatically - bulk emissions flow through the same publisher CoC, so an event that opts in is audited and reconcilable with no extra wiring and no double-logging. The three bulk-sourced events don't opt in in this release, so their bulk emissions are not logged.

5. Architecture & design principles​

5.1 The trigger standard: hook the process, not the table​

The defining architectural decision. A trigger belongs on the process method that causes the data change, firing after next (post-commit, or transactionally safe via the commit log) - not on the underlying table's insert()/update(). Table hooks: fire for every code path (data entities, OData, dual-write, imports, migrations - false positives), are silently skipped by set-based DML (update_recordset, doUpdate), tax hot paths, and are a semantic mismatch (a field write is not the business action).

The catalog follows this standard. Documented exceptions remain where they are genuinely correct:

  • Master data (Customer/Vendor) - no single orchestrating process method; the table is the only chokepoint.
  • Field-delta events (Customer Hold Status Changed) - intrinsically buffer-level (orig() vs current).
  • Posting-only ledger tables (AssetTrans, CustInvoiceJour, ProjInvoiceJour) - written exclusively by posting engines; no entity/import path exists, so the table hook is at-posting.
  • Scattered-write events (Warehouse Work Completed) - no single safe process funnel.

Every exception is annotated in code with its rationale and the set-based-DML caveat.

5.2 The three-file event pattern​

Each event = an event class (BusinessEventsBase, [BusinessEvents] attribute naming contract + labels + module), a [DataContract] contract (private fields, newFromXxx factory, parm methods with [DataMember] + [BusinessEventsDataMember]), and a CoC trigger. buildContract() is [Wrappable(true), Replaceable(true)] so ISVs and customers can extend payloads.

5.3 Payload conventions​

legalEntity first; enums as int and enum2Symbol string; business keys and resolved names over RecIds; secondary lookups (item name, worker name) resolved at build time so consumers don't call back.

5.4 Opt-in interfaces as the extension seam​

Two marker interfaces keep the platform generic and additive: IMGCLatticeBusinessEventLoggable (audit/replay opt-in) and IMGCLatticeBulkEventSource (bulk-resync registration). New events and new entities plug in by implementing an interface, not by editing the platform.

5.5 Packaging conventions​

A single model label file (@MGCLattice: ids); security privileges for the operational forms/actions, each reaching its roles through a duty (no privilege is granted to a role directly); module dependencies declared explicitly in the model descriptor (PersonnelCore, TaxEngine, UnitOfMeasure and Retail are required by specific events).


6. Integration scenarios (worked)​

ScenarioEventsReliability assistEndpoint pattern
Real-time CRM / CDP syncCustomer Created/Updated, Customer Hold Status Changedaudit + reconciliationEvent Grid → CRM
Master-data onboarding + DR resyncReleased Product Created, Customer/External Itembulk engine + auditService Bus → PIM/MDM
ASN / carrier integrationWarehouse Load Shipped, Sales Packing Slip Postedreconciliation (at-least-once)Service Bus → TMS/3PL
Quality hold & releaseQuality Order Passed / FailedreplayHTTPS → QMS/LIMS
Finance close automationGeneral Journal Posted, FTI/Project Invoice Postedpost-commit delegatePower Automate → close tooling
MES hand-offfull Production lifecycle (9 events)auditService Bus → MES
Procure-to-payPurchase Requisition Approved, Purchase Agreement Confirmed, Vendor Check IssuedreconciliationPower Automate / bank positive-pay
Treasury / positive-payVendor Check Issuedaudit + replaysecure file/API → bank
Commerce catalog distributionRetail Assortment Published (price-at-publish)auditService Bus → commerce

Audit, replay and reconciliation apply only to events that opt in through IMGCLatticeBusinessEventLoggable (§4.1). In this release only Retail Assortment Published (and the Business Event Publish Failed alert itself) opts in, so in the other scenarios those assists need the event opted in first.

6.1 The delivery-guarantee ladder​

The reliability pieces compose into a graduated guarantee - each rung catches what the one above missed:

best-effort publish (platform: no record, nothing to resend)
│
├─> AUDIT every opt-in publish logged Success/Error
│
├─> REPLAY operator/code re-sends a logged event by key
│
├─> RECONCILE hourly background process auto-resends Error rows
│
└─> EXHAUSTED / loud, visible terminal state after N retries -
MANUAL the DLQ-equivalent that previously did not exist

A customer chooses how far up the ladder to climb: activate events only (rung 0), or opt them into audit (rung 1). For an audited event that is also replayable, rungs 2 and 3 need nothing scheduled: the reconciliation background process runs hourly by itself, which gives at-least-once outbound delivery.


7. Deployment, activation & operations​

Activation (per event, zero code): System administration → Setup → Business events → Business events catalog → find by name/category → Activate → bind to an endpoint (Service Bus, Event Grid, HTTPS, Power Automate).

Operator runbook for the platform:

  1. Discover - run Business event catalog refresh, browse the explorer.
  2. Validate coverage - run Business event coverage report (seeds the reference list if empty).
  3. Turn on audit - ensure the events you care about implement IMGCLatticeBusinessEventLoggable (in this release, of the package's own events only Retail Assortment Published and Business Event Publish Failed do).
  4. Reconciliation runs by itself - the Lattice business event reconciliation background process re-publishes failed events hourly; nothing to schedule.
  5. Bootstrap a consumer - run the relevant Publish all … bulk job for initial load / DR resync.

Requirements: D365 F&O (version-agnostic against the public business-events framework). Module dependencies are explicit in the descriptor; PersonnelCore, TaxEngine, UnitOfMeasure, and Retail are required by specific events.


8. Adoption maturity model​

StageWhat the customer doesCapability used
0: NotifyActivate a handful of events to Power Automate/Teamscatalog
1: IntegrateWire events to Service Bus/Event Grid for system-to-systemcatalog + payload richness
2: ObserveTurn on audit; browse the catalog/exploreraudit (§4.1), catalog discovery (§4.3)
3: GuaranteeRely on the hourly reconciliation background process for at-least-once outbound (nothing to schedule)reconciliation sweep (§4.2)
4: BootstrapUse bulk resync for new consumers / DRbulk initial-load / resync (§4.4)
5: GovernRun coverage reports against the reference listcoverage report (§4.3)

Appendix A - full event index by domain (81)​

  • Vendor (6): Vendor Created, Vendor Updated, Vendor Hold Status Changed, Vendor Check Issued, Vendor Payment Posted, Vendor ASN Line Changed
  • Customer (7): Customer Created, Customer Updated, Customer Hold Status Changed, Customer Credit Limit Exceeded, Collection Case Changed, Collection Letter Posted, Dunning Level Changed
  • Production (9): Production Order Scheduled, Production Order Estimated, Production Order Started, Production Pick List Posted, Production Route Card Posted, Production Order Reported as Finished, Production Order Ended, BOM Calculation Completed, BOM Version Approved
  • Sales (10): Sales Order Confirmed, Sales Packing Slip Posted, Sales Order Invoice Posted, Sales Return Order Created, Return Order Received, Return Credit Note Posted, Sales Quotation Confirmed, Sales Order Canceled, Sales Order Line Changed, Sales Agreement Confirmed
  • Purchasing (3): Purchase Return Order Created, Purchase Requisition Approved, Purchase Agreement Confirmed
  • Inventory (10): Transfer Order Shipped, Transfer Order Received, Transfer Order Picking List Created, Inventory Closing Completed, Quality Order Passed, Quality Order Failed, Released Product Created, Inventory On-Hand Changed, Item Back In Stock, Picking List Canceled
  • Warehouse (3): Warehouse Load Shipped, Warehouse Work Completed, Warehouse Picking Completed
  • Finance (7): General Journal Posted, Free Text Invoice Posted, Fixed Asset Acquisition Posted, Fixed Asset Disposal Posted, Bank Reconciliation Completed, Advanced Bank Reconciliation Completed, E-invoice Submission Status Changed
  • Project (2): Project Invoice Posted, Project Task Assigned
  • Planning (1): MRP Run Completed
  • Retail & Product (3): Retail Assortment Published, External Item Changed, Retail Info-code Response Captured
  • Pricing & trade agreements (7): Sales Price Trade Agreement Changed, Sales Price Agreement Created, Sales Price Agreement Modified, Sales Price Agreement Superseded, Sales Price Agreement Deleted, Sales Price Agreement Effective, Sales Price Agreement Expired
  • Asset maintenance (4): Maintenance Work Order Created, Maintenance Work Order Scheduled, Maintenance Work Order Started, Maintenance Work Order Completed
  • Platform & integration (9): Batch Job Status Changed, DMF Execution Completed, Recurring Data Job Health Changed, Aggregate Measurement Refresh Completed, Workflow Lifecycle Action, Workflow Stalled or Failed, Integration Item Parked, Business Event Publish Failed, Custom Ad-hoc Business Event

Appendix B - supporting objects​

  • Tables (4): MGCLatticeBusinessEventLog, MGCLatticeBusinessEventReference, MGCLatticeBusinessEventCatalog, MGCLatticeBusinessEventCatalogMember
  • Enums (3): MGCLatticeBusinessEventLogStatus, MGCLatticeBusinessEventReconcileState, MGCLatticeBulkEventEntityType
  • Interfaces (2): IMGCLatticeBusinessEventLoggable, IMGCLatticeBulkEventSource
  • Operational forms/actions: Catalog Explorer, Catalog Refresh, Coverage Report, Reference Seed, Reconciliation Sweep, Publish-All (×3)
  • Security: the Catalog Explorer is in MGCLatticeIntegrationServiceMonitoringDuty (all three integration roles); the refresh, coverage report, reference seed, reconciliation sweep and the three Publish-All jobs are in MGCLatticeBatchMaintenanceDuty (integration administrator role only)

Appendix C: known gaps​

  • Per-event reference docs: every one of the 81 events has a page in the event reference.
  • No generated event contracts: this release generates no JSON-Schema or AsyncAPI contracts for the events; the event reference and the catalog explorer describe each payload.
  • Reconciliation scope: reconciliation resends events logged with Error. It does not detect an event a consumer never acknowledged, or an event that was expected but never raised (§4.2).
  • Verification: every event has an automated X++ test that raises it through its real trigger.

Per-event triggers and field-level payloads: the event reference.