# Dynamics 365

> Source: https://docs.clonepartner.com/connectors/dynamics365/

<!-- sources: src/connectors/dynamics365.ts, references/envoy/connectors.md -->

# Dynamics 365

The `dynamics365` connector talks to Microsoft Dynamics 365 / Dataverse over the OData Web API: streamed reads, CRUD writes, entity metadata, and custom table creation.

## When to use it

Use `dynamics365` to migrate or sync data into a Dataverse environment, or to provision custom tables and columns as part of a migration. Authentication is OAuth2 client credentials against Azure AD service principals.

## Configuration

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `base_url` | string | Yes | — | D365 environment URL (e.g. `https://org.crm.dynamics.com`) |
| `tenant_id` | string | Yes | — | Azure AD tenant ID |
| `credentials` | object[] | Yes | — | Service principal credentials (add multiple for higher throughput). Each item: `client_id` (required), `client_secret` (required, secret) |
| `api_version` | string | No | `v9.2` | OData API version |
| `rate_limit` | number | No | 10 | Max requests per second per credential |
| `bypass_plugins` | boolean | No | — | Send `MSCRM.BypassCustomPluginExecution` header |
| `bypass_business_logic` | boolean | No | — | Send `MSCRM.BypassBusinessLogicExecution` header |

A legacy single-credential form (top-level `client_id` and `client_secret`) is still accepted and treated as the first credential slot. At least one credential is required, or the connector fails with `D365 connector requires at least one credential (client_id + client_secret)`.

## Methods

| Method | Arguments | Returns | Behavior |
|---|---|---|---|
| `find` | `resource` (required), `filter`, `select`, `expand`, `top`, `order_by` | array | Streams records page by page, auto-following `@odata.nextLink` to the end |
| `find_one` | `resource` (required), `id` (required), `select`, `expand` | object | Gets a single record by GUID |
| `count` | `resource` (required), `filter` | object | Returns the matching record count via OData `$count` |
| `create` | `resource` (required), `data` (required) | object | Creates a record; the new GUID is read from the `OData-EntityId` response header |
| `update` | `resource` (required), `id` (required), `data` (required) | object | Updates a record by ID |
| `upsert` | `resource` (required), `key_field` (required), `key_value` (required), `data` (required), `prevent_create`, `prevent_update` | object | Upserts via alternate key; `prevent_create` sends `If-Match: *`, `prevent_update` sends `If-None-Match: *` |
| `delete` | `resource` (required), `id` (required) | void | Deletes a record by ID |
| `execute` | `path` (required), `method` (required), `data`, `headers` | object | Raw OData request for actions and functions |
| `fetch_metadata` | `entity` (required) | object | Pulls entity attribute metadata |
| `list_entities` | `refresh` | array | Lists all entity sets; `refresh: true` discards the cached metadata |
| `create_entity` | `schema_name` (required), `display_name` (required), `display_collection_name`, `description`, `ownership_type`, `primary_attribute`, `has_notes`, `has_activities`, `solution`, `metadata` | object | Creates a custom table via the Metadata API |
| `create_attribute` | `entity` (required), `schema_name` (required), `display_name` (required), `attribute_type`, `description`, `required_level`, `max_length`, `min_value`, `max_value`, `solution`, `metadata` | object | Adds a column to an existing entity |
| `create_relationship` | `schema_name` (required), `referenced_entity` (required), `referencing_entity` (required), `relationship_type`, `referenced_attribute`, `lookup`, `intersect_entity_name`, `solution`, `metadata` | object | Creates a 1:N or N:N relationship |
| `publish_all` | — | object | Publishes all customizations (`PublishAllXml`) |

`filter`, `select`, `expand`, and `order_by` are raw OData expressions (`$filter`, `$select`, `$expand`, `$orderby`), not the JSON filter syntax used by the SQL connectors.

## Example

```yaml
name: import_contacts_to_d365
connectors:
  destination:
    type: dynamics365
steps:
  - name: create_contact
    type: call_method
    config: |
      {
        "connector": "destination",
        "method": "create",
        "args": {
          "resource": "contacts",
          "data": { "firstname": "Ada", "lastname": "Lovelace" }
        }
      }
```

The matching `connectors.yaml` entry for CLI runs:

```yaml
destination:
  type: dynamics365
  config:
    base_url: https://your-org.crm.dynamics.com
    tenant_id: ${AZURE_TENANT_ID}
    credentials:
      - client_id: ${D365_CLIENT_ID}
        client_secret: ${D365_CLIENT_SECRET}
```

## Behavior notes

- Requests rotate round-robin across the configured credentials, each with its own OAuth token and per-second rate limit — add credentials to raise total throughput under Dataverse service-protection limits.
- Tokens are refreshed automatically before expiry; a `401` response triggers one forced token refresh and retry.
- `429` responses honor the `Retry-After` header and `5xx` responses use exponential backoff, up to 5 retries.
- On write, lookup fields are transformed automatically: GUID or alternate-key values on lookup attributes become `@odata.bind` navigation properties, and keys already ending in `@odata.bind` pass through unchanged.
- `bypass_plugins` and `bypass_business_logic` add the corresponding `MSCRM.*` headers to write requests, which requires the service principal to hold the matching Dataverse privileges.

## Related

- [Connectors overview](/connectors/) — all compiled connector types
- [Reading and writing data](/building/reading-and-writing-data/) — calling connector methods from steps
- [Idempotent sync and the mapping DB](/building/idempotent-sync/) — pattern for migration writes
