·11 min read·Rishi

Business Events and Data Events in D365 Finance & Operations: A Developer Guide

Business Events and Data Events in D365 Finance & Operations: A Developer Guide

If you are building integrations on Dynamics 365 Finance & Operations, Business Events and Data Events are the two mechanisms you need to understand. They replace the old pattern of polling OData endpoints or writing custom batch jobs to detect state changes. Events fire when something actually happens inside F&O, and external systems — Power Automate, Azure Service Bus, Event Grid, or your own HTTPS endpoint — react to them in near real-time.

This guide is aimed at developers. We will walk through the architecture, build a custom Business Event from scratch in X++, configure Data Events, and cover the integration patterns that actually work in production.

Business Events vs. Data Events

These two serve different purposes and fire at different layers.

Business Events are raised explicitly in X++ code when a business process completes — a purchase order is confirmed, a sales order is invoiced, a journal is posted. They carry a payload you define in a contract class. You decide when the event fires and what data it includes.

Data Events are raised automatically by the framework when a Dataverse virtual entity row is created, updated, or deleted. No custom X++ required. You configure them through the Business Events catalog in the UI.

Business EventsData Events
TriggerExplicit send() call in X++Automatic on CRUD operations
PayloadCustom contract classStandard entity fields
Custom code requiredYesNo
Use caseProcess milestones, domain-specific signalsRow-level change tracking
GranularityYou choose when to fireEvery insert/update/delete

Use Business Events when you need to signal that a business process reached a meaningful state. Use Data Events when you need to react to any change on a specific table/entity.

The Business Events Framework: How It Works

The framework has three layers:

  1. Event definition — an X++ class extending BusinessEventsBase paired with a contract class extending BusinessEventsContract
  2. Catalog and activation — events appear in the Business Events catalog at System administration > Setup > Business events > Business events catalog, where you activate them for specific legal entities and endpoints
  3. Delivery — the framework queues activated events and delivers them to configured endpoints via batch processing

Events are not delivered synchronously. They are queued and processed by the BusinessEventsBundleProcessor batch job. Default processing interval is one minute. This means your integrations should tolerate up to a minute of latency (sometimes more under heavy batch load).

Supported Endpoints

  • Azure Service Bus (Queue or Topic)
  • Azure Event Grid
  • Azure Blob Storage
  • Power Automate (via the built-in Fin & Ops connector)
  • HTTPS (custom webhook)
  • Azure Event Hubs

Each endpoint type is configured under System administration > Setup > Business events > Endpoints.

Building a Custom Business Event: Step by Step

Let us build a Business Event that fires when a custom approval process completes. We need three things: a contract class, a business event class, and a call site.

Step 1: The Contract Class

The contract defines the payload — what data the external system receives when the event fires.

[DataContract]
class ApprovalCompletedContract extends BusinessEventsContract
{
    private str 20    approvalId;
    private str 60    approvedByWorker;
    private str 20    documentNumber;
    private utcdatetime approvalDateTime;

    /// <summary>
    /// Initializes the contract from the approval table record.
    /// </summary>
    public static ApprovalCompletedContract newFromApprovalTable(ApprovalTable _approval)
    {
        ApprovalCompletedContract contract = new ApprovalCompletedContract();

        contract.initialize(_approval);

        return contract;
    }

    private void initialize(ApprovalTable _approval)
    {
        // Always call parmLegalEntity — the framework uses this for
        // legal-entity-scoped activation
        this.parmLegalEntity(
            CompanyInfo::findDataArea(_approval.DataAreaId).RecId
        );

        approvalId       = _approval.ApprovalId;
        approvedByWorker = HcmWorker::find(_approval.ApprovedBy).name();
        documentNumber   = _approval.DocumentNum;
        approvalDateTime = _approval.ApprovedDateTime;
    }

    [DataMember('ApprovalId')]
    public str 20 parmApprovalId(str 20 _value = approvalId)
    {
        approvalId = _value;
        return approvalId;
    }

    [DataMember('ApprovedByWorker')]
    public str 60 parmApprovedByWorker(str 60 _value = approvedByWorker)
    {
        approvedByWorker = _value;
        return approvedByWorker;
    }

    [DataMember('DocumentNumber')]
    public str 20 parmDocumentNumber(str 20 _value = documentNumber)
    {
        documentNumber = _value;
        return documentNumber;
    }

    [DataMember('ApprovalDateTime')]
    public utcdatetime parmApprovalDateTime(utcdatetime _value = approvalDateTime)
    {
        approvalDateTime = _value;
        return approvalDateTime;
    }
}

Key rules for the contract:

  • Extend BusinessEventsContract, not DataContract directly
  • Every field needs a DataMember attribute — this defines the JSON property name in the payload
  • Call parmLegalEntity() during initialization — without this, legal-entity-scoped activation will not work
  • Use a static newFrom* constructor pattern — it keeps initialization clean and testable

Step 2: The Business Event Class

[BusinessEvents(classStr(ApprovalCompletedContract),
    'Approval:Completed',
    'Approval process completed',
    ModuleType::AccountsPayable)]
class ApprovalCompletedBusinessEvent extends BusinessEventsBase
{
    private ApprovalTable approval;

    /// <summary>
    /// Creates a new instance from the approval record.
    /// </summary>
    public static ApprovalCompletedBusinessEvent
        newFromApprovalTable(ApprovalTable _approval)
    {
        ApprovalCompletedBusinessEvent businessEvent
            = new ApprovalCompletedBusinessEvent();

        businessEvent.approval = _approval;

        return businessEvent;
    }

    /// <summary>
    /// Builds the contract payload.
    /// Called by the framework during delivery.
    /// </summary>
    protected BusinessEventsContract buildContract()
    {
        return ApprovalCompletedContract::newFromApprovalTable(approval);
    }
}

The BusinessEvents attribute takes four parameters:

  1. Contract classclassStr(ApprovalCompletedContract)
  2. Event ID — a unique identifier string (use Category:Action convention)
  3. Description — shows in the catalog UI
  4. Module — determines which module the event appears under in the catalog

The buildContract() method is called by the framework at delivery time, not at send time. This matters: the approval record must still exist when the batch job processes the event.

Step 3: Sending the Event

At the point in your business logic where the approval completes, send the event:

public void completeApproval(ApprovalTable _approval)
{
    ttsbegin;

    _approval.Status = ApprovalStatus::Approved;
    _approval.ApprovedDateTime = DateTimeUtil::utcNow();
    _approval.update();

    // Send the business event
    ApprovalCompletedBusinessEvent businessEvent
        = ApprovalCompletedBusinessEvent::newFromApprovalTable(_approval);
    businessEvent.send();

    ttscommit;
}

Call send() inside the ttscommit block. If the transaction rolls back, the event is discarded — you do not want external systems reacting to something that never actually happened.

Step 4: Rebuild the Catalog

After deploying your code, rebuild the Business Events catalog so your new event appears:

  1. Navigate to System administration > Setup > Business events > Business events catalog
  2. Click Rebuild business events catalog (or Manage > Rebuild catalog)
  3. Your event should appear under the module you specified

You must rebuild the catalog every time you add a new business event class or change the BusinessEvents attribute. The catalog is cached — without a rebuild, the framework does not know your event exists.

Data Events: No Code Required

Data Events fire automatically when a Dataverse virtual entity row changes. They are configured entirely through the UI.

Setting Up a Data Event

  1. Go to System administration > Setup > Business events > Business events catalog
  2. Select the Data events tab
  3. Click New and choose the Dataverse virtual entity to monitor
  4. Select the operation: Create, Update, or Delete
  5. Activate the event for the legal entities and endpoint you want

The payload is the standard Dataverse entity schema — all fields from the virtual entity are included automatically.

When to Use Data Events vs. Business Events

Data Events are convenient but they fire on every qualifying CRUD operation. For a high-volume table, this generates a lot of events. Consider the numbers:

  • A VendInvoiceJour table that gets 500 inserts per day generates 500 Data Events per day
  • If you also track updates (status changes, payment posting), that number multiplies
  • Each event goes through the batch queue, consuming batch capacity

Use Data Events for low-to-medium volume entities where you need change tracking without writing X++. For high-volume scenarios or when you only care about specific state transitions (not every field update), write a custom Business Event instead.

Integration Patterns

Pattern 1: Power Automate with the Finance & Operations Connector

The simplest integration. Power Automate has a built-in trigger: When a Business Event occurs (Fin & Ops).

  1. Create a new cloud flow in Power Automate
  2. Select the Fin & Ops Apps connector
  3. Choose the trigger When a Business Event occurs
  4. Select your environment, legal entity, and event category
  5. The trigger payload maps directly to your contract's DataMember fields

From there you can route the event to Teams, Outlook, Dataverse, or any of the hundreds of Power Automate connectors.

One catch: the Power Automate endpoint uses the Dataverse integration layer. If your F&O environment has Dataverse integration issues (sync errors, virtual entity configuration problems), Power Automate triggers will silently fail. Check the Data management > Dataverse integration workspace if triggers stop firing.

Pattern 2: Azure Service Bus for Reliable Messaging

For production integrations that need guaranteed delivery, use Azure Service Bus.

{
  "BusinessEventId": "Approval:Completed",
  "ControlNumber": 1234567,
  "LegalEntity": "USMF",
  "EventTime": "2026-04-12T14:30:00Z",
  "Payload": {
    "ApprovalId": "APR-001234",
    "ApprovedByWorker": "John Smith",
    "DocumentNumber": "INV-2026-0456",
    "ApprovalDateTime": "2026-04-12T14:29:55Z"
  }
}

The JSON structure has a standard envelope (BusinessEventId, ControlNumber, LegalEntity, EventTime) plus your custom Payload from the contract.

On the consumer side, use an Azure Function or a service that reads from the Service Bus queue:

// Azure Function consuming the Business Event from Service Bus
[Function("ProcessApprovalCompleted")]
public async Task Run(
    [ServiceBusTrigger("approval-events", Connection = "ServiceBusConnection")]
    ServiceBusReceivedMessage message)
{
    var body = message.Body.ToString();
    var envelope = JsonSerializer.Deserialize<BusinessEventEnvelope>(body);

    if (envelope.BusinessEventId != "Approval:Completed")
        return;

    var payload = JsonSerializer
        .Deserialize<ApprovalCompletedPayload>(envelope.Payload);

    _logger.LogInformation(
        "Approval {Id} completed by {Worker} for document {Doc}",
        payload.ApprovalId,
        payload.ApprovedByWorker,
        payload.DocumentNumber);

    // Your integration logic here — update external system,
    // send notification, trigger downstream process
    await _externalService.NotifyApprovalAsync(payload);
}

Pattern 3: Event Grid for Fan-Out

When multiple systems need to react to the same event, use Azure Event Grid. Configure the Event Grid endpoint in F&O, then add subscriptions in Azure for each consumer — an Azure Function, a Logic App, a webhook, another Service Bus queue.

This avoids the pattern of configuring the same Business Event multiple times in F&O for different endpoints. Configure it once to Event Grid, fan out on the Azure side.

Troubleshooting

Event Is Not Appearing in the Catalog

  • You forgot to rebuild the catalog — Manage > Rebuild catalog
  • The BusinessEvents attribute is missing or has incorrect parameters
  • The class does not extend BusinessEventsBase
  • The contract class does not extend BusinessEventsContract

Event Is Activated But Not Firing

  • Check that the BusinessEventsBundleProcessor batch job is running and not stuck
  • Verify the legal entity matches — if you activated for USMF but the transaction runs in USRT, the event will not fire
  • Confirm send() is called inside a committed transaction — if the ttscommit never executes, the event is discarded
  • Check System administration > Setup > Business events > Business events errors for delivery failures

Event Fires But Endpoint Does Not Receive It

  • For Service Bus: validate the connection string and queue/topic name in the endpoint configuration
  • For Power Automate: check the Dataverse integration workspace for sync errors
  • For HTTPS: the endpoint must return 200 within 10 seconds or the delivery is marked as failed
  • Check the Active events grid — look at the Last processed timestamp to confirm the batch job is picking up events

High Latency Between Event and Delivery

The batch processing interval is the bottleneck. Default is one minute, but under heavy batch load it can be longer. Options:

  • Increase the batch thread allocation for BusinessEventsBundleProcessor
  • Move the business events batch group to a dedicated batch server
  • For critical real-time scenarios, consider whether Business Events are the right mechanism — they are near-real-time, not real-time

Key Takeaway

Business Events and Data Events give you a clean, supported way to push state changes out of D365 F&O to external systems. Business Events require X++ but give you full control over when they fire and what data they carry. Data Events require no code but fire on every CRUD operation, which can be noisy on high-volume entities.

For most developer-built integrations, the pattern is: write a custom Business Event for each meaningful state transition in your domain, configure Azure Service Bus as the endpoint for reliable delivery, and consume the events in Azure Functions. Reserve Data Events for lightweight change tracking on low-volume entities where writing X++ is not justified.

The framework is mature and production-tested, but do not skip the troubleshooting basics — rebuild the catalog after deployment, verify the batch job is running, and check the errors log when things go quiet.

Comments

No comments yet. Be the first!