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:
-
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.
-
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.
| Dimension | Native D365 F&O | MGCLattice |
|---|---|---|
| Process-level event coverage | ~sparse, notification-leaning | 81 events, 16 modules |
| Trigger altitude | n/a (you build it) | process method, post-commit-safe |
| Payload | varies | enriched: human-readable enums, resolved names, amounts, keys |
| Outbound audit trail | none | every opt-in publish logged with outcome |
| Replay of a failed/lost publish | none | per-event replay from the audit log |
| Automatic recovery | none | hourly reconciliation background process |
| Catalog introspection | manual | discovery → explorer + coverage report |
| Initial load / DR resync | hand-rolled per project | generic, 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.productionStatusproductionStatusName. 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)
| Event | Business moment | Trigger (process hook) | Payload highlights |
|---|---|---|---|
| Vendor Created | A vendor master record is created | VendTable.insert() (master-data exception) | account, name, group, currency |
| Vendor Updated | A vendor master record changes | VendTable.update() (exception) | account, name, group, currency |
| Vendor Hold Status Changed | A vendor's hold is applied, changed or released, or a vendor is created on hold | VendTable Updated data event → Blocked changed; VendTable.insert() → new vendor on hold | account, name, new and previous hold status (+names), new-vendor flag |
| Vendor Check Issued | A check is issued to a vendor | BankChequeTable.update() → ChequeStatus Created/NotPrinted/Testprint → Payment, RecipientType==Vend | bank account, check number, payee name and account, amount, currency, date, voucher |
| Vendor Payment Posted | A vendor payment journal posts | LedgerJournalCheckPost.runInternalPostJournalTransactionEnded (post-commit delegate) → posted Payment journal, one event per vendor posting | journal, voucher, vendor, amount signed from the vendor's side, debit/credit, currency, payment method, reference, date |
| Vendor ASN Line Changed | A purchase ASN line is created, or its quantity changes | WHSASNItem.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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Customer Created | Customer master created | CustTable.insert() (exception) | account, name, group |
| Customer Updated | Customer master changes | CustTable.update() (exception) | account, name, group |
| Customer Hold Status Changed | Blocked/credit status changes | CustTable.update() field-delta | account, name, blocked status (+name), credit limit |
| Customer Credit Limit Exceeded | The standard credit limit check fails | CustCreditLimit.check() returns false | account, name, the credit limit in effect for the check, balance, order balance and total exposure (accounting currency), triggering context |
| Collection Case Changed | A collections case is opened or closed | CustCollectionsCaseDetail::createCase() / closeCase() | customer, case id, status (+name), action, balance, event date (the user's current date) |
| Collection Letter Posted | A collection letter is posted | CustCollectionLetterPost.writeCustCollectionLetterJour() → letter status Posted | customer, letter number, level (+name), letter and due dates, fee, currency, voucher, posting date |
| Dunning Level Changed | A letter changes an open transaction's collection letter level | CustCollectionLetterJour.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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Production Order Scheduled | Status → Scheduled | ProdUpdScheduling.run(); ReqTransPoMarkFirm.insertMovement() (planned order firmed as Scheduled) | order, item (+name), status (+name) |
| Production Order Estimated | Status → CostEstimated | ProdUpdCostEstimation.run() | order, item |
| Production Order Started | Status → StartedUp | ProdUpdStartUp.run() | order, item (+name), status (+name) |
| Production Pick List Posted | Picklist journal posts | ProdJournalCheckPostBOM.run() | order, journal, item, status (+name), posted component lines (item, quantity, unit, operation) |
| Production Route Card Posted | Route-card journal posts | ProdJournalCheckPostRoute.run() | order, journal, item, status (+name), posted lines (operation, resource, hours, good/error qty) |
| Production Order Reported as Finished | Status → ReportedFinished | ProdUpdReportFinished.run(); ProdJournalCheckPostProd.run() (report-as-finished journal) | order, item, status (+name), total good/error qty reported finished, unit |
| Production Order Ended | Status → Completed | ProdUpdHistoricalCost.run() | order, item, status (+name), total good/error qty reported finished, unit |
| BOM Calculation Completed | A cost calc row is produced | BomCalcItem.insertBOMCalcTable() | item, cost price, calc date |
| BOM Version Approved | A BOM/formula version is approved | BomVersionApprove.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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Sales Order Confirmed | Confirmation journal posts (not pro forma) | SalesConfirmJournalPost.postJournalPost() | order, customer, confirm id, currency, total |
| Sales Packing Slip Posted | Packing slip posts (excl. pro forma, cancellations and return order receipts) | SalesPackingSlipJournalPost.postJournalPost() | order, customer, packing slip, delivery date |
| Sales Order Invoice Posted | A sales order invoice or credit note posts (not pro forma) | SalesInvoiceJournalPost.postJournalPost() | invoice account, order, invoice, date, amount, currency |
| Sales Return Order Created | A return order is created | SalesTableType_ReturnItem.inserted() | return order, customer, reason |
| Return Order Received | A return order line is registered (goods arrived) | SalesLine.returnLineRegisterUpdate() / SalesReturnNonStockedRegistrationHelper.updateOnRegistration() → ReturnStatus → Registered | RMA, order, item, quantity, disposition code, warehouse |
| Return Credit Note Posted | A return order is invoiced (its credit note posts) | SalesInvoiceJournalPost.postJournalPost() → return order (SalesType ReturnItem) | credit note, return order, RMA, customer, amount, currency |
| Sales Quotation Confirmed | A quotation is confirmed | SalesQuotationEditLinesForm_Sales_Confir.createJournal() | quotation, confirmation, sales order created, customer, confirm date |
| Sales Order Canceled | Status → Canceled | SalesUpdateRemain.cancelOpenOrderLinesDeliveryRemainder() / cancelMultipleOrdersDeliveryRemainder() / MCRSalesOrderCancellation::cancelOrder() / cancelOrderFTC() | order, customer, status (+name), sales type (+name), RMA number |
| Sales Order Line Changed | A line's quantity, price or requested dates change | SalesLine Updated data event | order, item, line, previous/current quantity, price, receipt and ship dates, currency |
| Sales Agreement Confirmed | A sales agreement snapshot is taken | AgreementConfirm_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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Purchase Return Order Created | A purchase return is created | PurchTableType_ReturnItem.inserted() | return order, vendor, reason |
| Purchase Requisition Approved | A requisition reaches Approved | PurchReqWFStatusTransitionHelper::setPurchReqTable2Approved() (workflow) | requisition, name, requester, buying legal entities |
| Purchase Agreement Confirmed | A purchase agreement snapshot | AgreementConfirm_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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Transfer Order Shipped | Transfer status → Shipped | InventTransferUpdShip.run() | transfer, from/to warehouse, status (+name) |
| Transfer Order Received | Transfer status → Received | InventTransferUpdReceive.run() | transfer, from/to warehouse, status (+name) |
| Transfer Order Picking List Created | Generate picking list creates a transfer picking list | InventTransferUpdPick.run() | route, order, trans type (+name), route status, lines |
| Inventory Closing Completed | An inventory close or recalculation finishes | InventCostLedgerPostingTask.updateLedgerPosting() | closing date, voucher, close or recalculation |
| Quality Order Passed | Quality order → Pass | InventQualityOrderValidate.run() | quality order, item, test group, status (+name) |
| Quality Order Failed | Quality order → Fail | InventQualityOrderValidate.run() (shared) | + disposition code, blocking qty, worker, route |
| Released Product Created | A product is released to a legal entity | EcoResProductReleaseManager.createInventTable() / EcoResProductReleaseManagerItem.release() | item, product RecId, search name, type (+name), lifecycle state |
| Inventory On-Hand Changed | Physical on-hand changes | InventUpdateOnhand.onOnHandUpdated (delegate); sent after the transaction commits | item, site, warehouse, physical change, on-hand quantities, unit |
| Item Back In Stock | Available physical moves from 0 or less to above 0 | InventUpdateOnhand.onOnHandUpdated (delegate); sent after the transaction commits | item, site, warehouse, previous/current available, threshold, unit |
| Picking List Canceled | A picking list or a single picking line is canceled | WMSPickingRoute.cancel() (CoC) + post-handler on WMSOrderTransType_Output_OrderPick.cancel | route, 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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Warehouse Load Shipped | A load is ship-confirmed, on its own or through its transport load | WHSShipConfirm.shipConfirm(WHSLoadId); WHSShipConfirmationActionTransportLoad.shipConfirm() | load, shipment, carrier, status (+name) |
| Warehouse Work Completed | WHS work closes | WHSWorkTable.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 Completed | Picking work closes | WHSWorkTable.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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| General Journal Posted | A daily journal posts | LedgerJournalCheckPost.runInternalPostJournalTransactionEnded (post-commit delegate) | journal, name, posting date |
| Free Text Invoice Posted | An FTI posts (not pro forma) | CustInvoiceJour.insert() (posting-only exception) | invoice, customer, date, amount, currency |
| Fixed Asset Acquisition Posted | Acquisition transaction posts | AssetTrans.insert() → Acquisition (exception) | asset, book, amount, date, voucher, reversal/reclassification flags |
| Fixed Asset Disposal Posted | Disposal transaction posts | AssetTrans.insert() → DisposalSale/Scrap (exception) | asset, book, amount, date, voucher, reversal/reclassification flags |
| Bank Reconciliation Completed | A bank statement is reconciled (classic Reconcile account) | BankAccountReconcile.updateBankAccountStatementToReconciled() → ReconcileDate first set | bank account, statement number and date, currency, ending balance, reconcile date, matched count and amount |
| Advanced Bank Reconciliation Completed | An Advanced bank reconciliation worksheet is marked as reconciled | BankReconciliationHeader::reconcile() / reconcileInBatch() → IsReconciled No → Yes | reconciliation, 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 Changed | An Electronic invoicing submission is logged or changes status | BusinessDocumentSubmission Inserted, or Updated when State changes | document 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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Project Invoice Posted | A project invoice posts (not pro forma) | ProjInvoiceJour.insert() (posting-only exception) | project, invoice, invoice account, amount, currency, date |
| Project Task Assigned | A resource is assigned to a project task (work breakdown structure), or an assignment row is written by a customization or import | ProjTask::reserveResourceActivity(); ProjActivityAssignment Inserted/Updated data events | project, activity, resource, worker, assigned hours, load %, capacity-limited flag, action |
3.10 Planning (1)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| MRP Run Completed | A 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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Retail Assortment Published | An assortment is published for the first time in a legal entity, or its effective item list changes | RetailAssortmentLookupTask.run() | assortment, customer, per-item price-at-publish, item list |
| External Item Changed | A cust/vend external item cross-ref changes | CustVendExternalItem Inserted/Updated data events | item, external id, module type (+name), relation, dim |
| Retail Info-code Response Captured | An info code (reason code) response is recorded in headquarters | RetailTransactionInfocodeTrans.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)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Sales Price Trade Agreement Changed | Any sales-price agreement row is inserted or updated | PriceDiscTable 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 Created | A posted journal line adds a price with none in effect on its From date | PriceDiscAdmCheckPost.createOrUpdatePriceDiscTableRecords() | shared price payload: change type (+name), account and item relation, currency, unit, quantity break, dimensions, dates, amount, record id |
| Sales Price Agreement Modified | A posted journal line changes an existing price | PriceDiscAdmCheckPost.createOrUpdatePriceDiscTableRecords() | shared price payload + old amount, changed fields |
| Sales Price Agreement Superseded | A posted journal line adds a price where one was in effect on its From date | PriceDiscAdmCheckPost.createOrUpdatePriceDiscTableRecords() | shared price payload + the superseded price's amount and record id |
| Sales Price Agreement Deleted | A price is deleted by a user, by code or by a posted journal | PriceDiscTable.delete() / PriceDiscAdmTable::deleteMarked() | shared price payload |
| Sales Price Agreement Effective | A price's From date is reached | MGCLatticeTradeAgreementPriceEffectivityBatch.run() (daily background process) | shared price payload |
| Sales Price Agreement Expired | A price's To date passes with no other price of the agreement in effect | MGCLatticeTradeAgreementPriceEffectivityBatch.run() (daily background process) | shared price payload |
3.13 Asset maintenance (4)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Maintenance Work Order Created | A work order is created | EntAssetWorkOrderTable Inserted data event, raised when the creating transaction commits | work order, type, description, lifecycle state, functional location, assets, workers, expected/scheduled/actual dates |
| Maintenance Work Order Scheduled | It moves to the lifecycle model's scheduled state | EntAssetLifecycleStateUpdate_WorkOrderTable.updateStateRecord() | as Created |
| Maintenance Work Order Started | It enters a state that sets the actual start | EntAssetLifecycleStateUpdate_WorkOrderTable.updateStateRecord() | as Created |
| Maintenance Work Order Completed | It enters a state that sets the actual end | EntAssetLifecycleStateUpdate_WorkOrderTable.updateStateRecord() | as Created |
3.14 Platform & integration (9)
| Event | Business moment | Trigger | Payload highlights |
|---|---|---|---|
| Batch Job Status Changed | A batch job's status changes through the job record, unless a batch job event filter excludes the job | BatchJob.update() (+ batch job event filter) | job, caption, company, status and previous status (+names), start/end, task count |
| DMF Execution Completed | A data project entity execution reaches Finished, Error or Canceled | DMFDefinitionGroupExecution.update() / DMFEntityDbExporter::generateEntityDBDataV2() | execution, project, entity, outcome, target/staging status (+names), record and error counts |
| Recurring Data Job Health Changed | The 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 pages | job, 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 Completed | A full-reset entity store refresh finishes | BIMeasurementProcessingStatisticsFull.update() | measurement, refresh type, status (+name), succeeded, start/completion, duration |
| Workflow Lifecycle Action | A reportable workflow action is recorded | WorkflowTrackingCommentTable.insert() | action, tracking type (+name), workflow, document, step, acting user, assigned-to user, comment, UTC time |
| Workflow Stalled or Failed | A workflow faults, or a work item waits past the threshold | MGCLatticeWorkflowStalledAlertBatch.run() (hourly background process) | alert reason, workflow, document, step, assigned user, age, status |
| Integration Item Parked | A failed Lattice async operation or file ingestion item is parked in the dead-letter queue | MGCLatticeIntegrationDlqAlert.onParked() | DLQ id, source, service and operation, error code, status, attempts, first failure time, message |
| Business Event Publish Failed | Publishing an opted-in Lattice event fails outside a transaction | BusinessEventsPublisher.publish() (Lattice wrapper, see §4.1) | failed event id and class, direction, identifier, failure details, log RecId, UTC time |
| Custom Ad-hoc Business Event | Customer X++ code raises it | MGCLatticeCustomAdHocEventApi::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 (plusgetEventIdentifier/getEventIdentifierType) to opt in. No other wiring.MGCLatticeBusinessEventsPublisher_Class_Extension- a CoC wrapper onBusinessEventsPublisher.publishthat, in afinallyblock, writes aMGCLatticeBusinessEventLogrow 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 nocatch, 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.replayFromLogrebuilds 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.
MGCLatticeBusinessEventLogis extended withReconcileState(NotReconciled / Resolved / Exhausted / ManualRequired),RetryCount,LastRetryDateTime.MGCLatticeBusinessEventReconciliationSweepruns 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 viareplayFromLog(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 everyBusinessEventsBasecarrying the[BusinessEvents]attribute via the supportedDictClass::getAttributedClasses(...)- the same path the platform's ownBusinessEventsCatalogHelperuses (no internal/metadata-provider access). It populatesMGCLatticeBusinessEventCatalog(event inventory: category, contract, payload-field count) andMGCLatticeBusinessEventCatalogMember(the payload schema, one row per[DataMember]/[BusinessEventsDataMember]).MGCLatticeBusinessEventCatalogExplorer- a master-detail form to browse the catalog and each event's payload.- Coverage validation:
MGCLatticeBusinessEventReferenceholds an external reference list (a 211-entry D365 F&O business-events catalog is embedded, deployable with no runtime file dependency), andcoverageReport()reports both gap directions - reference events absent from this environment, and discovered events not in the reference. A one-clickMGCLatticeBusinessEventCoverageReportrefreshes, 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 underSysOperationSandbox. - 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)
| Scenario | Events | Reliability assist | Endpoint pattern |
|---|---|---|---|
| Real-time CRM / CDP sync | Customer Created/Updated, Customer Hold Status Changed | audit + reconciliation | Event Grid → CRM |
| Master-data onboarding + DR resync | Released Product Created, Customer/External Item | bulk engine + audit | Service Bus → PIM/MDM |
| ASN / carrier integration | Warehouse Load Shipped, Sales Packing Slip Posted | reconciliation (at-least-once) | Service Bus → TMS/3PL |
| Quality hold & release | Quality Order Passed / Failed | replay | HTTPS → QMS/LIMS |
| Finance close automation | General Journal Posted, FTI/Project Invoice Posted | post-commit delegate | Power Automate → close tooling |
| MES hand-off | full Production lifecycle (9 events) | audit | Service Bus → MES |
| Procure-to-pay | Purchase Requisition Approved, Purchase Agreement Confirmed, Vendor Check Issued | reconciliation | Power Automate / bank positive-pay |
| Treasury / positive-pay | Vendor Check Issued | audit + replay | secure file/API → bank |
| Commerce catalog distribution | Retail Assortment Published (price-at-publish) | audit | Service 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:
- Discover - run Business event catalog refresh, browse the explorer.
- Validate coverage - run Business event coverage report (seeds the reference list if empty).
- 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). - Reconciliation runs by itself - the Lattice business event reconciliation background process re-publishes failed events hourly; nothing to schedule.
- 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
| Stage | What the customer does | Capability used |
|---|---|---|
| 0: Notify | Activate a handful of events to Power Automate/Teams | catalog |
| 1: Integrate | Wire events to Service Bus/Event Grid for system-to-system | catalog + payload richness |
| 2: Observe | Turn on audit; browse the catalog/explorer | audit (§4.1), catalog discovery (§4.3) |
| 3: Guarantee | Rely on the hourly reconciliation background process for at-least-once outbound (nothing to schedule) | reconciliation sweep (§4.2) |
| 4: Bootstrap | Use bulk resync for new consumers / DR | bulk initial-load / resync (§4.4) |
| 5: Govern | Run coverage reports against the reference list | coverage 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 inMGCLatticeBatchMaintenanceDuty(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.