> For the complete documentation index, see [llms.txt](https://docs.cybus.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.cybus.io/2-4-2/guides/system-connectivity/mes-erp-business-systems/microsoft-dynamics-365-integration.md).

# Microsoft Dynamics 365 Integration

This guide describes how to integrate Microsoft Dynamics 365 with Connectware. You configure a service commissioning file that creates rows in a Microsoft Dataverse table from shop floor data and polls a Dataverse table with an OData query in return. A complete example file is available at the end of this guide.

## Objectives

* Establishing an OAuth-authenticated connection between Connectware and the Dataverse Web API.
* Creating rows in a Dataverse table from shop floor data.
* Polling a Dataverse table with an OData query.

## Prerequisites

To follow this guide, you will need the following:

* A running instance of Cybus Connectware.
* A Microsoft Dynamics 365 environment that stores its data in Microsoft Dataverse (for example, Sales, Customer Service, or Field Service).
* Administrator privileges in Microsoft Entra ID to register an application, and in the Power Platform admin center to create an application user.
* Access to the [Admin UI](/2-4-2/access/admin-ui.md) with sufficient [user permissions](/2-4-2/access/user-management.md).
* Basic knowledge of MQTT and the Connectware [services](/2-4-2/data-flows/services.md) concept (for example, [service commissioning files](/2-4-2/data-flows/service-commissioning-files.md), [connections](/2-4-2/data-flows/service-commissioning-files/resources/cybus-connection.md), and [endpoints](/2-4-2/data-flows/service-commissioning-files/resources/cybus-endpoint.md)).

## Connectware and Microsoft Dynamics 365 Integration

Dynamics 365 applications such as Sales, Customer Service, and Field Service store their business data in Microsoft Dataverse. Dataverse exposes this data through the [Web API](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/compose-http-requests-handle-errors), an OData v4 REST interface available at `https://<environment>.api.crm.dynamics.com/api/data/v9.2/`. Connectware communicates with the Web API through the [HTTP/REST connector](/2-4-2/connectors/enterprise-connectors/http-rest.md), which handles token retrieval and renewal automatically.

This guide uses a custom Dataverse table named Quality Event as an example:

* **Shop floor to Dynamics 365**: Machines publish quality events to MQTT topics, and Connectware creates one table row per event. Quality engineers then work with the records in Dynamics 365, in Power Automate flows, or in Power BI reports.
* **Dynamics 365 to shop floor**: Connectware polls the table with an OData query and publishes the result to an MQTT topic, so shop floor systems receive changes made in Dynamics 365.

The same patterns apply to any Dataverse table, standard or custom.

The MQTT topics in this guide follow an ISA-95-style equipment hierarchy (`<enterprise>/<site>/<area>/<line>/<cell>`). The mappings subscribe with wildcards across all levels, so any machine in the hierarchy is picked up without changing the integration.

### App Registration and Application User

Connectware authenticates against the Dataverse Web API as a service principal, using the OAuth 2.0 client credentials grant with Microsoft Entra ID. This requires an app registration in Microsoft Entra ID and an application user in your Dataverse environment:

1. Register an application in Microsoft Entra ID and create a client secret for it. Record the Application (client) ID, the Directory (tenant) ID, and the client secret. For detailed steps, see [Use single-tenant server-to-server authentication](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/use-single-tenant-server-server-authentication) in the Microsoft documentation.
2. In the Power Platform admin center, create an application user that is bound to the app registration and assign it a security role that grants create and read privileges on the target table. For detailed steps, see [Create an application user](https://learn.microsoft.com/en-us/power-platform/admin/manage-application-users#create-an-application-user) in the Microsoft documentation.
3. Look up the Web API endpoint of your environment in Power Apps under **Settings** > **Developer resources**. For more information, see [View developer resources](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/view-download-developer-resources) in the Microsoft documentation.

### Dynamics 365 Connection Properties

The connection to the Dataverse Web API requires values from your environment and your app registration. We add them as parameters to the service commissioning file, so you can set them when you install the service.

Do not worry about copying the service commissioning file snippets together into one, the complete example file is available at the end of this guide.

* `dataverseApiHost`: The host name of the Web API endpoint of your environment, without the scheme. For example, `example.api.crm.dynamics.com`.
* `environmentUrl`: The URL of your environment, shown as the instance URL on the **Developer resources** page. Connectware requests the OAuth token for this URL. For example, `https://example.crm.dynamics.com`.
* `tenantId`: The Directory (tenant) ID of your Microsoft Entra ID tenant.
* `clientId` and `clientSecret`: The Application (client) ID and the client secret of the app registration.
* `entitySetName`: The entity set name of the target table. For example, `cr123_qualityevents`.
* `pollInterval`: The polling interval for the Dataverse table in milliseconds.
* `topicRoot`: The root of the MQTT topic hierarchy. Defaults to `enterprise`.

{% code lineNumbers="true" %}

```yaml
parameters:
  dataverseApiHost:
    description: Host name of the Dataverse Web API of your environment, without the scheme
    type: string
    default: example.api.crm.dynamics.com

  environmentUrl:
    description: URL of your Dataverse environment, used as the token audience
    type: string
    default: https://example.crm.dynamics.com

  tenantId:
    description: Directory (tenant) ID of your Microsoft Entra ID tenant
    type: string
    default: 00000000-0000-0000-0000-000000000000

  clientId:
    description: Application (client) ID of the app registration
    type: string

  clientSecret:
    description: Client secret of the app registration
    type: string

  entitySetName:
    description: Entity set name of the Dataverse table
    type: string
    default: cr123_qualityevents

  pollInterval:
    description: Polling interval for the Dataverse table in milliseconds
    type: integer
    default: 30000

  topicRoot:
    description: Root of the MQTT topic hierarchy
    type: string
    default: enterprise
```

{% endcode %}

### Dynamics 365 Connection

To connect to the Dataverse Web API, we set up a `Cybus::Connection` resource that uses the HTTP/REST connector with the [OAuth 2.0 Client Credentials Grant](https://docs.cybus.io/2-4-2/guides/system-connectivity/mes-erp-business-systems/pages/Hij6TsDxB4GxfyynBwLn#oauth-2.0-client-credentials-grant). The `prefix` property applies the Web API base path `/api/data/v9.2` to all endpoints of the connection, so the endpoint paths only contain the entity set names. The `headers` property adds the [headers that Microsoft recommends](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/compose-http-requests-handle-errors#http-headers) for all Dataverse Web API requests.

{% code lineNumbers="true" %}

```yaml
resources:
  dynamicsConnection:
    type: Cybus::Connection
    properties:
      protocol: Http
      connection:
        scheme: https
        host: !ref dataverseApiHost
        port: 443
        prefix: /api/data/v9.2
        headers:
          Accept: application/json
          OData-MaxVersion: '4.0'
          OData-Version: '4.0'
        oauthClientCredentials:
          auth_url: !sub 'https://login.microsoftonline.com/${tenantId}/oauth2/token'
          client_id: !ref clientId
          client_secret: !ref clientSecret
          audience: !ref environmentUrl
```

{% endcode %}

{% hint style="warning" %}
Microsoft Entra ID must know which API a requested token is for. The [v2.0 token endpoint](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow) expects this information in a `scope` parameter (`https://<environment>.crm.dynamics.com/.default`), which the `oauthClientCredentials` property does not provide. This guide therefore uses the [v1.0 token endpoint](https://learn.microsoft.com/en-us/previous-versions/azure/active-directory/azuread-dev/v1-oauth2-client-creds-grant-flow) (`https://login.microsoftonline.com/<tenant-id>/oauth2/token`), which identifies the target API through a `resource` value instead. The `audience` property of the connection carries the environment URL for this purpose. If the connection does not reach the **Connected** state and the [service logs](/2-4-2/data-flows/services/service-logs/service-logs-of-individual-services.md) show an `AADSTS` error from the token endpoint, your Connectware version does not transmit this value with the token request. In that case, use a static access token as described in [Using a Static Access Token](#using-a-static-access-token).
{% endhint %}

#### Using a Static Access Token

You can authenticate with a token that you obtain outside of Connectware instead of the `oauthClientCredentials` property. Request the token from the v1.0 token endpoint of your tenant, with the environment URL as the `resource` value:

{% code lineNumbers="true" %}

```bash
curl -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/token" \
  -d "grant_type=client_credentials" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}" \
  -d "resource=https://example.crm.dynamics.com"
```

{% endcode %}

Replace `${TENANT_ID}`, `${CLIENT_ID}`, and `${CLIENT_SECRET}` with the values of your app registration, and `https://example.crm.dynamics.com` with the URL of your environment. Copy the `access_token` value from the response and pass it as an `Authorization` header in the connection instead of the `oauthClientCredentials` property:

{% code lineNumbers="true" %}

```yaml
headers:
  Accept: application/json
  OData-MaxVersion: '4.0'
  OData-Version: '4.0'
  Authorization: !sub 'Bearer ${accessToken}'
```

{% endcode %}

Access tokens from Microsoft Entra ID expire after about one hour, so this option is only suitable for testing and troubleshooting.

### Creating Dataverse Rows from Shop Floor Data

To [create a table row](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/create-entity-web-api), the Dataverse Web API expects a POST request to the entity set of the table, with the column values as the JSON request body. To find the entity set name, open your table in Power Apps and select **Advanced** > **Tools** > **Copy set name**.

We define a write endpoint for the entity set and a mapping that feeds it from the MQTT topic hierarchy. The HTTP/REST connector expects the request body in the `body` property of the message (see [Publishing Data to REST Servers](/2-4-2/connectors/enterprise-connectors/http-rest.md#publishing-data-to-rest-servers)). The `transform` rule wraps the incoming payload accordingly, so machines can publish their data without knowing about this convention.

{% code lineNumbers="true" %}

```yaml
qualityEventWriteEndpoint:
  type: Cybus::Endpoint
  properties:
    protocol: Http
    connection: !ref dynamicsConnection
    write:
      path: !sub '/${entitySetName}'

qualityEventWriteMapping:
  type: Cybus::Mapping
  properties:
    mappings:
      - subscribe:
          topic: !sub '${topicRoot}/+site/+area/+line/+cell/quality-events'
        publish:
          endpoint: !ref qualityEventWriteEndpoint
        rules:
          - transform:
              expression: '{ "body": $ }'
```

{% endcode %}

Any message published to a matching topic, for example `enterprise/hamburg/assembly/line-1/press-01/quality-events`, now creates one row in the table. The property names in the payload are the logical names of the table columns, in lowercase and including the publisher prefix. The examples in this guide use the placeholder prefix `cr123`, replace it with the prefix of your solution publisher:

{% code lineNumbers="true" %}

```json
{
  "cr123_name": "Surface scratch on housing",
  "cr123_line": "line-1",
  "cr123_defecttype": "scratch",
  "cr123_measuredvalue": 0.8
}
```

{% endcode %}

Dataverse responds with the status `204 No Content` and returns the URI of the created row in the `OData-EntityId` response header. Connectware publishes the result of every request to the `/res` topic of the endpoint.

### Polling a Dataverse Table with an OData Query

To read data from Dynamics 365, we define a subscribe endpoint that polls the table at the configured interval (see [Subscribing to Data](/2-4-2/connectors/enterprise-connectors/http-rest.md#subscribing-to-data-continuous-polling)). The `query` property adds [OData query options](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query/overview) to the GET request: `$select` limits the returned columns, `$filter` returns only active rows, and `$orderby` with `$top` returns the ten most recently changed rows.

{% code lineNumbers="true" %}

```yaml
qualityEventPollEndpoint:
  type: Cybus::Endpoint
  properties:
    protocol: Http
    connection: !ref dynamicsConnection
    subscribe:
      path: !sub '/${entitySetName}'
      interval: !ref pollInterval
      query:
        $select: 'cr123_name,cr123_line,cr123_defecttype,cr123_measuredvalue,modifiedon'
        $filter: 'statecode eq 0'
        $orderby: 'modifiedon desc'
        $top: '10'

qualityEventPollMapping:
  type: Cybus::Mapping
  properties:
    mappings:
      - subscribe:
          endpoint: !ref qualityEventPollEndpoint
        publish:
          topic: !sub '${topicRoot}/dynamics-365/quality-events'
```

{% endcode %}

Connectware wraps every poll result in a JSON structure with a timestamp (see [Response Message Format](/2-4-2/connectors/enterprise-connectors/http-rest.md#response-message-format)). The rows of the table arrive in the `value` array of the OData response:

{% code lineNumbers="true" %}

```json
{
  "timestamp": 1752657600000,
  "value": {
    "@odata.context": "https://example.api.crm.dynamics.com/api/data/v9.2/$metadata#cr123_qualityevents(cr123_name,cr123_line,cr123_defecttype,cr123_measuredvalue,modifiedon)",
    "value": [
      {
        "cr123_name": "Surface scratch on housing",
        "cr123_line": "line-1",
        "cr123_defecttype": "scratch",
        "cr123_measuredvalue": 0.8,
        "modifiedon": "2026-07-16T09:00:00Z"
      }
    ]
  }
}
```

{% endcode %}

## Verifying the Integration

1. Install the service and set the parameters to the values of your environment and your app registration.
2. Check that the connection is in the **Connected** state on the service details page in the Admin UI. If the connection does not reach the connected state, check the [service logs](/2-4-2/data-flows/services/service-logs/service-logs-of-individual-services.md) for errors from the token endpoint.
3. Publish a test message to `enterprise/hamburg/assembly/line-1/press-01/quality-events`, for example with an MQTT client or the Admin UI.
4. Open the table in Power Apps or in your Dynamics 365 app and check that a new row appears with the values of the test message.
5. Use the [Data Explorer](/2-4-2/monitoring/data-explorer.md) to check that the poll result arrives on the topic `enterprise/dynamics-365/quality-events` at the configured interval.
6. The result of every HTTP request is published to the `/res` topic of the endpoint. If Dataverse rejects a request, the message on the `/res` topic contains an `error` property with the HTTP status.

## Service Commissioning File Example

{% file src="/files/EeQcHm39w0V6k0qz2Ki2" %}

{% code title="microsoft-dynamics-365-example.yml" lineNumbers="true" expandable="true" %}

```yaml
---
# ----------------------------------------------------------------------------
# Service Commissioning File
# ----------------------------------------------------------------------------
# Copyright: Cybus GmbH
# Contact: support@cybus.io
# ----------------------------------------------------------------------------
# Microsoft Dynamics 365 Integration (Example)
# ----------------------------------------------------------------------------

description: >
  Service commissioning file for the bidirectional integration between
  Connectware and Microsoft Dynamics 365 (Example)

metadata:
  name: Microsoft Dynamics 365 Integration
  provider: cybus
  homepage: https://www.cybus.io
  version: 1.0.0

parameters:
  dataverseApiHost:
    description: Host name of the Dataverse Web API of your environment, without the scheme
    type: string
    default: example.api.crm.dynamics.com

  environmentUrl:
    description: URL of your Dataverse environment, used as the token audience
    type: string
    default: https://example.crm.dynamics.com

  tenantId:
    description: Directory (tenant) ID of your Microsoft Entra ID tenant
    type: string
    default: 00000000-0000-0000-0000-000000000000

  clientId:
    description: Application (client) ID of the app registration
    type: string

  clientSecret:
    description: Client secret of the app registration
    type: string

  entitySetName:
    description: Entity set name of the Dataverse table
    type: string
    default: cr123_qualityevents

  pollInterval:
    description: Polling interval for the Dataverse table in milliseconds
    type: integer
    default: 30000

  topicRoot:
    description: Root of the MQTT topic hierarchy
    type: string
    default: enterprise

resources:
  # Connection to the Dataverse Web API using OAuth 2.0 client credentials
  dynamicsConnection:
    type: Cybus::Connection
    properties:
      protocol: Http
      connection:
        scheme: https
        host: !ref dataverseApiHost
        port: 443
        # Applied to the paths of all endpoints of this connection
        prefix: /api/data/v9.2
        # Headers recommended by Microsoft for all Dataverse Web API requests
        headers:
          Accept: application/json
          OData-MaxVersion: '4.0'
          OData-Version: '4.0'
        oauthClientCredentials:
          # Microsoft Entra ID v1.0 token endpoint of your tenant
          auth_url: !sub 'https://login.microsoftonline.com/${tenantId}/oauth2/token'
          client_id: !ref clientId
          client_secret: !ref clientSecret
          # The environment URL identifies the target API of the token
          audience: !ref environmentUrl

  # Creates one row in the Dataverse table per MQTT message
  qualityEventWriteEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Http
      connection: !ref dynamicsConnection
      write:
        path: !sub '/${entitySetName}'

  qualityEventWriteMapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            topic: !sub '${topicRoot}/+site/+area/+line/+cell/quality-events'
          publish:
            endpoint: !ref qualityEventWriteEndpoint
          rules:
            - transform:
                expression: '{ "body": $ }'

  # Polls the Dataverse table with an OData query
  # Replace the cr123 prefix with the publisher prefix of your table
  qualityEventPollEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Http
      connection: !ref dynamicsConnection
      subscribe:
        path: !sub '/${entitySetName}'
        interval: !ref pollInterval
        query:
          $select: 'cr123_name,cr123_line,cr123_defecttype,cr123_measuredvalue,modifiedon'
          $filter: 'statecode eq 0'
          $orderby: 'modifiedon desc'
          $top: '10'

  qualityEventPollMapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            endpoint: !ref qualityEventPollEndpoint
          publish:
            topic: !sub '${topicRoot}/dynamics-365/quality-events'
```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.cybus.io/2-4-2/guides/system-connectivity/mes-erp-business-systems/microsoft-dynamics-365-integration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
