| name | business-events-authoring |
| description | Author or extend custom Business Events in D365 Finance & Operations. Invoke when the user asks to "create a business event", "add a business event", "build a custom business event", or wire D365FO outbound notifications to Power Automate / Service Bus / Event Grid. |
| applies_when | User intent mentions business events, BusinessEventsBase, BusinessEventsContract, outbound notifications, Power Automate triggers, Service Bus events, or Event Grid from D365FO. |
⛔ NEVER write X++ AOT XML files directly via PowerShell, terminal file commands (Set-Content, Out-File, New-Item), editor write tools, or any raw text approach. The XML schema is proprietary. ALWAYS use d365fo generate … commands to produce correct AOT XML. If d365fo is unavailable in PATH, stop and ask the user to install it.
Business Events Authoring in D365FO
Business events are the standard D365FO mechanism for outbound event-driven
notifications. They decouple D365FO from subscribers: Power Automate, Azure
Service Bus, Azure Event Grid, Logic Apps, or any HTTP endpoint can receive
them without polling or custom integration code.
Reference: https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/business-events/home-page
Pattern overview
A custom business event consists of exactly two classes:
1. Event class — extends BusinessEventsBase
- Decorated with [BusinessEvents(...)] — registers it in the catalog
- Implements buildContract() to populate the payload
- Has a static newFrom<Context>(...) factory
2. Contract class — implements BusinessEventsContract
- Decorated with [DataContractAttribute]
- One parmXxx() accessor per payload field, decorated with [DataMemberAttribute]
Both classes are X++ AxClass XML files. Use d365fo generate business-event to scaffold them correctly.
Pre-flight
d365fo search business-event <namePart> --output json
d365fo get business-event <ExistingName> --output json
d365fo get table <PrimaryTable> --output json
Scaffolding
d365fo generate business-event CustPaymentBusinessEvent \
--contract-name CustPaymentBusinessEventContract \
--payload "custAccount:CustAccount" \
--payload "paymentAmount:AmountCur" \
--payload "currencyCode:CurrencyCode" \
--category "CustomerPayments" \
--primary-table CustTrans \
--out c:/AOT/MyModel/AxClass/CustPaymentBusinessEvent.xml \
--out-contract c:/AOT/MyModel/AxClass/CustPaymentBusinessEventContract.xml
Event class skeleton
[BusinessEvents(
classStr(CustPaymentBusinessEvent),
classStr(CustPaymentBusinessEventContract),
"CustomerPayments",
"@MyModel:CustPaymentBusinessEventDescription")]
public final class CustPaymentBusinessEvent extends BusinessEventsBase
{
private CustTrans custTrans;
// Factory method — called from the business process that fires the event
public static CustPaymentBusinessEvent newFromCustTrans(CustTrans _custTrans)
{
var event = new CustPaymentBusinessEvent();
event.parmCustTrans(_custTrans);
return event;
}
private void parmCustTrans(CustTrans _custTrans)
{
custTrans = _custTrans;
}
// Required: populate the contract from the current record context
[Wrappable(true), Replaceable(true)]
public BusinessEventsContract buildContract()
{
var contract = new CustPaymentBusinessEventContract();
contract.parmCustAccount(custTrans.AccountNum);
contract.parmPaymentAmount(custTrans.AmountCur);
contract.parmCurrencyCode(custTrans.CurrencyCode);
return contract;
}
}
Contract class skeleton
[DataContractAttribute]
public final class CustPaymentBusinessEventContract extends BusinessEventsContract
{
private CustAccount custAccount;
private AmountCur paymentAmount;
private CurrencyCode currencyCode;
[DataMemberAttribute('CustAccount')]
public CustAccount parmCustAccount(CustAccount _custAccount = custAccount)
{
custAccount = _custAccount;
return custAccount;
}
[DataMemberAttribute('PaymentAmount')]
public AmountCur parmPaymentAmount(AmountCur _paymentAmount = paymentAmount)
{
paymentAmount = _paymentAmount;
return paymentAmount;
}
[DataMemberAttribute('CurrencyCode')]
public CurrencyCode parmCurrencyCode(CurrencyCode _currencyCode = currencyCode)
{
currencyCode = _currencyCode;
return currencyCode;
}
}
Firing the event
Call the static factory from the business process at the right lifecycle point — typically in a table insert / update override, a posting engine, or a workflow action:
// In CustTrans.insert() CoC or a posting service method:
[ExtensionOf(tableStr(CustTrans))]
final class CustTrans_MyExt
{
public void insert()
{
next insert();
// Fire after successful insert
BusinessEventsBase::publish(
CustPaymentBusinessEvent::newFromCustTrans(this));
}
}
Grounding rule: always run d365fo find coc CustTrans::insert --output json first to check for existing CoC wrappers before adding a new one.
Activation lifecycle
After scaffolding and compiling:
- System Administration > Business events catalog — the event appears after a browser refresh or
iisreset.
- Activate — select the event, click Activate, choose the legal entity scope.
- Configure endpoint — click Endpoints, create or reuse a Service Bus / Event Grid / HTTP / Power Automate connection.
- Test — trigger the business process; the event payload arrives at the endpoint within seconds.
Hard rules
[BusinessEvents(...)] must be on the event class declaration — not on methods. Four arguments: classStr(EventClass), classStr(ContractClass), "CategoryString", "@File:DescriptionLabel".
buildContract() is called by the framework — return the populated contract instance; never return null.
- Contract
parmXxx() accessors must be decorated with [DataMemberAttribute] — the serialization layer uses these to build the JSON payload.
- Use EDTs for payload fields (e.g.
CustAccount, AmountCur) — not primitive types. Run d365fo get edt <Name> to confirm the EDT exists.
- Never call
BusinessEventsBase::publish() inside a ttsbegin/ttscommit block unless you intend to publish on rollback too. Call it after the outermost ttscommit or in the postInsert/postUpdate framework hook.
- Category string is free text — use a meaningful module-scoped category (e.g.
"CustomerPayments", "InventoryEvents") so the catalog is navigable.