DEV Community

Abel
Abel

Posted on • Originally published at Medium on

OmniSync: Dynamics 365 Integration with Salesforce and Fabric (Part 4)

A hands-on walkthrough of syncing Dynamics 365 using Dataverse, PowerApps, Plugins, and Azure Logic Apps as part of a multi-platform integration with Salesforce and Microsoft Fabric.

Posts in this Series

Introduction

Dynamics 365 Sales was used as the second CRM in the OmniSync integration architecture, synchronized alongside Salesforce. While both platforms offer rich and overlapping CRM functionality, Dynamics 365 was chosen for this role due to its tight integration with Microsoft ecosystem on Power Platform, Dataverse and its extensive market usage.

As with Salesforce, the goal wasn’t to cover the full feature set, but rather to align a core retail centric subset of entities across systems.

Retail Entities & Tradeoffs

Instead of using Dynamics 365 Commerce (the retail vertical specialized product with features like POS, checkout, payment features amongst others), we used the standard Sales module , which still offered the necessary capabilities with the exception of Stores, which we added manually.

To simplify the implementation and reduce system coupling, several tradeoffs were made, much like in the Salesforce setup:

  • Orders were created directly, bypassing Opportunities or Leads for simplicity.
  • Order Numbers are aligned manually with Salesforce to avoid complex sequencing logic.
  • Product Custom Fields were used (e.g., Size, Weight, Brand) avoiding the built-in Product Properties model.
  • Price Books include standard pricing and cost fields and Price List Items were not used.
  • Product Families and Product Groups were skipped, instead, categories were stored as simple product-level fields.
  • Discounts and many optional fields were excluded from synchronization.
  • Multi-currency support was simplified by using EUR only.
  • Unused fields were ignored to keep the system lightweight and avoid unnecessary sync complexity.

Customization

PowerApps & Dataverse

Dynamics 365 Sales is built on top of Microsoft’s Power Platform , which includes Dataverse (as the backend database) and PowerApps for customizing tables and UI. This architecture allows for easy and scalable customization without needing to write full applications.

In our case, Dynamics 365 didn’t include a Retail Store entity by default, since it’s part of more specialized modules like D365 Commerce. So we created it from scratch using Dataverse and customized its forms, menus, and behavior through PowerApps.

Dataverse Table

Using the PowerApps interface, we defined a new Retail Store table in Dataverse with all necessary fields. This is done visually through a form-based interface where you can drag, drop, and define field types quickly.

You can also use Quick Create for a faster setup, or even Copilot , which suggests fields based on prompts accelerating the initial setup.

Forms

After creating the table, PowerApps automatically generates default forms and views, which can then be tailored to fit your needs.

This is part of building a Model-Driven App , where the UI is dynamically generated based on the underlying data structure in Dataverse. You can adjust layouts, control visibility, define validation rules, and group fields as needed.

A customized form for the Retail Store form was created and later updated, with clearly organized sections for General Info and a brand new customized Address Details tabs.

Views

Views were also created to define how lists of records are presented. Those are displayed in a table or grid format with options for sorting, filtering, column selection, and search functionality.

Menus

To make this new entity usable within the Dynamics 365 Sales Hub, we added it to the app’s main navigation on the left pane. This lets users browse and manage retail stores directly, just like any standard CRM entity.

In addition, a Quick Create menu was set up to allow rapid record creation from anywhere in the app, streamlining the user experience.

UI Result

All these configurations resulted in a fully functional and visually integrated Retail Store form inside Dynamics 365. Users can now view, create, and edit these records natively, just like standard entities like Accounts or Products.

In-App Notifications

Logic was added to display a notification when a synchronization conflict is detected between Salesforce and Dynamics 365.

This is handled by a JavaScript function tied to the OnLoad event of the form. It checks the value of a hidden field Status_Sync. If the value is “Conflict”, a warning banner is displayed at the top of the form, and the field becomes visible for transparency.

This helps inform users that a manual review or correction should be needed to review and fix.

function SyncStatus(executionContext) {
    try {
        var formContext = executionContext.getFormContext();
        var status = formContext.getAttribute("omnisync_syncstatus").getValue();
        var fieldControl = formContext.getControl("omnisync_syncstatus");

        if (status === "Conflict") {
            formContext.ui.setFormNotification(
              "There has been a Conflict Syncing. Review the fields or contact the Administrator!", 
              "WARNING", 
              "conflictNotification"
            );
            fieldControl.setVisible(true);
        } else {
            formContext.ui.clearFormNotification("conflictNotification");
            fieldControl.setVisible(false);
        }
    } catch (error) {
        console.error("Error in displayFormNotification: ", error.message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Plugins

To maintain feature parity with Salesforce, we implemented a Dataverse Plugin to calculate the Sales Order Line Number on creation.

Plugins in Dataverse are written in C# and deployed to execute during specific events, like Create or Update. In our case:

  • The plugin runs in the Post Operation stage (after the record is created).
  • It is configured as Synchronous , so the line number is set immediately and is available in subsequent operations.

This mirrors the logic used in Salesforce and ensures consistency across systems.

Plugins are powerful tools, and for developers with a C# background, they provide a familiar and efficient way to enforce server-side business logic that goes beyond what low-code flows can achieve.

Logic Apps

As with the Salesforce integration, Logic Apps serves as the central iPaaS (Integration Platform as a Service) for Dynamics 365.

A Dataverse trigger which relies on the Dataverse connector , on the When a row is added, modified or deleted event is used. The action type (Insert, Update, or Delete) is then filtered within Logic Apps and handled by three separate Logic Apps , each dedicated to a specific type of change.

Why This Approach?

This design decision ( to split Logic Apps by action type ) was based on challenges encountered with Dataverse’s webhook-based triggering system like:

  • Some change events were duplicated
  • Others were missed entirely

By assigning one Logic App per change type (Insert, Update, Delete), the integration became far more stable and predictable. Additionally, this setup makes it easier to integrate Change Data Capture (CDC) logic for forwarding events to Microsoft Fabric via Event Hub.

Example: Insert — Order Product (Sales Order Line Item)

Let’s walk through a typical Logic App that listens for Order Product (Sales Order Line item) inserts:

  1. Trigger The Logic App is triggered when a new Order Products record is inserted in Dataverse.
  2. Integration User Check A condition checks if the change originated from an integration user to avoid triggering synchronizations from system to system updates.
  3. Retrieve Related Entities If valid, the Logic App retrieves related records: the product, the parent order, the synced Salesforce Order from the mapping table
  4. Parallel Branching Two parallel operations begin: Send CDC to Fabric through Event Hub and Create Salesforce Order Product if not already synced
  5. Conflict Check Before inserting in Salesforce, it checks if a line item already exists (via MasterDataMapping). If so, it sets the Sync_Status to conflict and exits.
  6. Salesforce Item Creation If not in conflict, the Salesforce PriceBookEntry is fetched, and the Order Product is created using the Salesforce connector.
  7. Sending CDC Finally, a new CDC record is generated and sent to Fabric to reflect this change in the Lakehouse system.

Data Transformations

Just like in the Salesforce integration, data transformations are a key part of the Logic Apps workflows. These ensure each system receives the correct structure and format of data.

Liquid Templates for Fabric CDC

To construct CDC messages for Fabric, like it was done in the SalesForce integration Liquid templates were used. These templates convert Dataverse outputs into the JSON schema expected by Fabric’s Event Stream.

Here’s an example template for syncing a Retail Store ** ** entity:

{
  "Operation": "{{ content.SdkMessage | capitalize }}",
  "Entity": "Store",
  "Values": {
    "StoreCode": "{{ content.cr989_storecode }}",
    "CustomerKey": "{{ content._cr989_account_value }}",
    "StoreTypeID": "{{ content.cr989_storetype }}",
    "StoreType": "{{ content._cr989_storetype_label }}",
    "StoreName": "{{ content.cr989_storename }}",
    "StoreDescription": "{{ content.cr989_storedescription }}",
    "StorePhone": "{{ content.cr989_storephone }}",
    "StoreFax": "{{ content.cr989_storefax }}",
    "AddressLine1": "{{ content.cr989_addressline1 }}",
    "EmployeeCount": "{{ content.cr989_employeecount }}",
    "Latitude": "{{ content.address1_latitude }}",
    "Longitude": "{{ content.address1_longitude }}",
    "CreatedDate": "{{ content.createdon }}",
    "UpdatedDate": "{{ content.modifiedon }}",
    "D365Id": "{{ content.ItemInternalId }}"
  },
  "CreatedDate": "{{ "now" | date: "%Y-%m-%d %H:%M" }}",
  "UpdatedDate": "{{ "now" | date: "%Y-%m-%d %H:%M" }}"
}
Enter fullscreen mode Exit fullscreen mode

This lets us dynamically construct messages that Fabric can ingest through Spark Streaming, keeping the transformation logic declarative and lightweight.

Direct Mapping via Connectors

While Liquid templates are used for constructing CDC payloads to Fabric, most of the remaining data transformations are done directly via Logic Apps connectors , particularly when syncing between Dataverse and Salesforce.

Because these connectors expose the full set of fields and metadata, mapping becomes a matter of:

  • Mapping fields from source to destination
  • Applying expression functions when necessary (e.g., formatting dates, converting units)
  • Structuring nested data (e.g., lookups or relationships)

This approach keeps the integration visual, low-code, and easy to maintain, especially when no complex transformation logic is required.

Deletions

Handling deletions in Dynamics 365 introduces a particular challenge: you don’t automatically get information about who deleted the record in the Logic App trigger (i.e., there’s no DeletedBy field in the payload).

This creates a problem, especially in a system like OmniSync, where we need to distinguish between:

  • Deletions made by users
  • Deletions triggered by integration logic

Without that information, the system could potentially enter a sync loop or process deletions unnecessarily. To prevent this, all Logic Apps include a consistent check to determine whether the change came from an Integration User.

Solution: Auditing via Audit Table

To work around this limitation, the Dataverse Audit table was used. By enabling auditing on the entities you want to monitor, you can retrieve historical changes, including who performed a Delete , Insert , or Update.

The process works like this:

  1. Enable auditing for each table where you want to track deletes or updates.
  2. After a deletion trigger fires, query the Audit table to identify the actor behind the change.
  3. If the user is an integration identity, the Logic App will skip further action to avoid recursion.

This same pattern is also applied to Insert and Update operations, helping reinforce idempotency and cycle prevention across the system.

Viewing Audit Logs

You can quickly inspect audit logs using the Audit Summary View in Power Platform’s admin interface.

For more detailed filtering and querying, using FetchXML is a better option, which we’ll explore in an upcoming section.

OmniSync Configuration Table

To avoid repetitive lookups and keep Logic Apps as streamlined as possible, a central configuration table was introduced in Dataverse: OmniSyncConfiguration.

This table holds key constants and settings used across the sync process, for example:

  • Dynamics 365 Integration User ID
  • Salesforce Standard Price Book ID
  • Other sync-related identifiers or constants that would otherwise require querying each time

Instead of hardcoding these values into Logic Apps (which can be harder to maintain), those are stored once and reused so can be used wherever needed via simple lookups.

This approach:

  • Keeps the logic clean and centralized
  • Improves performance by reducing connector calls
  • Makes the solution easier to maintain or extend in future

Tools Used in Dynamics 365 Integration

To support the setup, customization, and data management of Dynamics 365 during the OmniSync project, several key tools were used, most notably from the XrmToolBox.

XrmToolBox

XrmToolBox is a Windows-based tool that connects to Dataverse and offers over 30 built-in plugins, along with more than 300 additional plugins available for installation, supporting tasks ranging from customization and configuration to data manipulation.

It’s an essential tool for speeding up admin and integration tasks, especially in environments like Dynamics 365 where multiple entities and relationships are involved.

FetchXML Builder

This XrmToolBox plugin was incredibly helpful for exploring Dataverse tables quickly. With FetchXML , you can:

  • View rows and filter by field values
  • Select only relevant columns
  • Inspect relationships between entities visually

FetchXML is an XML-based query language for Dataverse that allows users to retrieve, filter, and sort data. It’s commonly used in Power Platform tools for advanced data querying and working with entity relationships.

While Power Apps Studio offers similar features, FetchXML Builder is far faster and more powerful when you’re exploring the data model or debugging issues.

SQL 4 CDS

For more advanced queries, SQL 4 CDS lets you write and run familiar SQL-style queries (SELECT, UPDATE, INSERT, DELETE) directly against your Dataverse instance.

This is perfect when working with complex joins or bulk-edit scenarios, and ideal if you’re already comfortable with SQL.

Configuration Migration Tool (CMT)

To prepare the system for deployment and migration across environments, we used the Configuration Migration Tool (CMT) via the Power Platform CLI using the command:

pac tool cmt
Enter fullscreen mode Exit fullscreen mode

This tool lets you:

  • Export schema and data from your current Dataverse environment
  • Re-import that structure into a new one (e.g., a QA or Production instance)

This was essential for seeding initial values like Stores, Accounts, especially to keep the systems aligned with Salesforce and Fabric.

⚠️While this project used a single environment, in a multi-environment setup (Dev/QA/Prod), each system would need coordinated seed data to prevent mismatches.

There are other of plenty plugins within XMlToolBox to do this task too, so you have lots of choices here.

Security

As mentioned in earlier parts of this series, an Integration User pattern is used across platforms to avoid synchronization loops and isolate system activity from human interaction.

In Dynamics 365, this same approach was applied. A dedicated integration user account was created and assigned a custom security role called Integration Role. This role was created using the least privilege principle , granting only the necessary permissions to access and modify the required entities (e.g., Accounts, Orders, Products, Retail Stores).

Client Applications (Microsoft Entra)

Two client applications were registered in Microsoft Entra ID to handle authentication for external services:

Azure Functions — Salesforce Updates

These apps are responsible for pushing updates to Salesforce (CDC responses) and were covered in detail in the previous article on SalesForce.

GitHub CI/CD Integration

This app is used to authenticate GitHub workflows during the CI/CD pipeline for deploying changes to Dynamics 365.

It will be described in more detail in the next section.

Git CI/CD Solutions

All Dynamics 365 customizations and configurations are stored in a Power Platform Solution , which is the standard packaging format for Dataverse based apps. This solution format allows you to version, export, and deploy your app components consistently across environments.

The source code for the solution is tracked in GitHub , enabling CI/CD automation.

Deployment Pipeline

A basic CI/CD setup was created using GitHub Actions , following a simple flow:

  1. Export to Git : Manual actions export the solution from Dynamics 365 to our GitHub source code
  2. Pull Request Workflow : Changes are reviewed and merged into the main or master branch via a pull request
  3. Deployment to Production : Once merged, GitHub Actions deploy the solution to a target environment (e.g., Production) using managed solution packaging

⚠️ Important Notes:

  • This pipeline requires a non-trial environment , as GitHub Actions create managed solutions , which aren’t supported in trial instances.
  • These actions do not include data by default. If you need to migrate seed data (e.g., stores, categories, users), use a tool like the Configuration Migration Tool (CMT) described earlier.

Monitoring

As detailed in the Salesforce article, all OmniSync components are monitored through Azure Monitor , with centralized logs and metrics available via a Log Analytics Workspace.

This approach offers both platform-native observability and extended diagnostics when needed.

Logic Apps Monitoring

Logic Apps include built-in run history tracking. From the Azure Portal, you can easily review:

  • Trigger invocations
  • Action steps
  • Success/failure details
  • Execution timings

This is particularly useful for spotting transient failures, checking retry logic, or auditing historical executions.

Plugin Trace Viewer (Power Platform)

To monitor plugin executions (like those used for setting Sales Order Line Numbers), we used the Plugin Trace viewer in Power Platform.

This built-in viewer lets you:

  • Inspect plugin events
  • See exceptions and execution order
  • Trace variable values and processing time

Plugin Trace Viewer (XrmToolBox)

For more advanced filtering and visibility, we also used the XrmToolBox Plugin Trace Viewer. Compared to the default Power Platform view, this plugin allows:

  • Full-text search across logs
  • Time range filters
  • Easy export and comparison

Application Insights Limitation (Trial Environment)

Since this OmniSync instance used a Trial Dataverse environment , integration with Application Insights wasn’t supported.

In a production or managed solution scenario, Application Insights could be used for deeper monitoring, telemetry, and diagnostics, including tracking performance metrics, usage patterns, and exceptions.

Lessons Learned

Working with Dynamics 365 as part of the OmniSync architecture highlighted several important technical and practical takeaways, especially in the context of integrating with external systems like Salesforce and Fabric.

  • UI Customization: one of the best surprises I found in this project was how easy it was to customize screens and forms in Dynamics 365 using PowerApps and Dataverse. Creating entirely new entities like Retail Stores from scratch complete with custom fields, forms, and navigation menus experiences was entirely visual, fast to iterate, and required no code.The Power Platform’s integration with Dataverse makes it seamless to link UI components directly to backend tables, and define user flows with precision, all from the same interface.
  • Permissions: even with a minimal integration user role, fine-tuning permissions over time was necessary, especially for less obvious operations, such as reading dynamic product properties or creating order-related entities. For example, enabling privileges like prvReadDynamicProperty wasn’t something that was immediately apparent. Despite this, using a least-privilege model for the integration user proved to be a solid approach, although it did require iteration and updates whenever permission related errors occurred.
  • Complex Lookups: for simple entities, Logic Apps with basic field mapping worked well. But more complex scenarios like Sales Order syncing required multi-step lookups to fetch like previously synced entities from the MasterDataMapping table related values like PriceBook entries or foreign key references like Account or Product IDs. Trying to avoid these lookups often led to loss of accuracy or sync integrity. In the end, it was better to embrace complexity where necessary and structure the workflows accordingly.
  • Dataverse plugins: provided a robust way and easy way through C# to implement logic (like calculating line numbers) where out-of-the-box workflows fell short.
  • Tools: XrmToolBox and its plugins FetchXML Builder and SQL 4 CDS dramatically accelerated development and debugging. Without them, exploring relationships, building queries, or inspecting tables would have taken much longer through standard web interfaces.

Final Thoughts and What’s Next

This concludes the current series of articles. While this marks the end for now, there may be a near future installment expanding on the topics covered, potentially including a video and an integration with SAP. Stay tuned for updates!

👀 Follow me here on Medium

💻 Source code: https://github.com/zodraz/omnisync-d365

Top comments (0)