Microsoft Dynamics 365 CRM ExecModule
Overview
MicrosoftDynamics365Module connects ValkyrAI workflows to the Microsoft Dataverse Web API v9.2 through the native map I/O ExecModule ABI. It creates, reads, lists, updates, and guarded-deletes accounts, contacts, leads, and opportunities without exposing OAuth credentials to workflow payloads.
The connector extends the useful account operations in n8n's Microsoft Dynamics CRM node across the four core sales entities. It validates one trusted organization origin, allowlists mutation and selection fields, bounds OData requests and responses, retries only reads, applies optional ETag concurrency checks, and requires explicit confirmation before deletion.
Usage
- Register a Microsoft Entra application or delegated identity for the target Dynamics environment.
- Grant only the Dynamics security-role privileges required for the selected entity operations.
- Store the OAuth access token in the encrypted
IntegrationAccount.apiKeyorpasswordSecureField. - Store the organization origin in
IntegrationAccount.accountId, for examplehttps://acme.crm.dynamics.com. - Set the account to
READYand bind it throughExecModuleConfig.authConfig.integrationAccount. - Use list operations to discover UUIDs, then pass those UUIDs to get, update, or guarded-delete operations.
Workflow inputs cannot supply authorization headers, alternate hosts, client secrets, refresh tokens, or arbitrary Dataverse entity sets.
Inputs
| Name | Type | Requirement | Default | Constraints |
|---|---|---|---|---|
operation | string | Required | None | One of the twenty operations below. |
organizationUrl | string | Optional | IntegrationAccount.accountId | HTTPS global Dynamics host such as https://acme.crm.dynamics.com; no path, port, query, credentials, or fragment. |
recordId | string | Get, update, and delete | None | Canonical Dataverse UUID; braces are normalized. |
record | object or JSON | Create and update | None | Non-empty, at most 50 allowlisted scalar fields and 256 KiB. |
select | string | Optional read projection | Provider default | 1–50 comma-separated allowlisted fields. |
filter | string | Optional list filter | None | Bounded OData expression, at most 4,096 characters; nested $ options and unsupported characters are rejected. |
orderBy | string | Optional list ordering | Provider default | One allowlisted field plus optional asc or desc. |
cursor | string | Optional list continuation | None | Opaque $skiptoken returned as nextCursor, at most 4,096 characters. |
limit | integer | Optional | 100 | 1–5,000 rows in one provider page. |
etag | string | Optional update/delete concurrency | None | Bounded strong or weak ETag sent as If-Match. |
confirmDelete | boolean | Delete operations | false | Must be exactly true before a DELETE is sent. |
timeoutMs | integer | Optional | 30000 | 100–300,000 ms for connect, read, write, and total call time. |
Create requirements:
- Accounts require
name. - Contacts require
lastname. - Leads require
subjectandlastname. - Opportunities require
name.
Writable fields are intentionally narrower than the entire tenant schema. Common addresses, phone and email fields, descriptions, revenue/value fields, stage/status fields, and selected @odata.bind relationships are available. Server-owned UUIDs, fullname, createdon, and modifiedon are read-only. Unknown fields and nested objects or arrays fail validation before transport.
Outputs
| Name | Type | When present | Description |
|---|---|---|---|
status | string | Always | success or error. |
operation | string | Always | Normalized operation name. |
resource | string | Valid operation | accounts, contacts, leads, or opportunities. |
attempts | integer | Always | Provider attempts consumed. |
httpStatus | integer | Provider responded successfully | Last HTTP status. |
data | object | Single-resource or mutation success | Provider representation or an accepted mutation receipt. |
id | string | Provider or request supplies identity | Canonical Dataverse UUID. |
items / count | array / integer | List success | Bounded rows and emitted count. |
hasMore | boolean | List success | Whether Dataverse returned another page. |
nextCursor | string | Another page exists | Validated opaque skip token. |
error | object | Failure | Safe code, message, retryability, and attempt count. |
OAuth tokens, authorization headers, and credential-like provider messages are redacted from outputs, logs, and workflow events.
IntegrationAccount Requirements
| Setting | Requirement |
|---|---|
| Provider | Microsoft Dynamics 365 / Dataverse |
accountName | Human-readable automation identity |
accountId | Full global Dynamics organization origin, for example https://acme.crm.dynamics.com |
apiKey or password | Encrypted SecureField containing the OAuth access token |
status | Exactly READY |
The OAuth grant must target the Dynamics organization resource and the bound principal must have a Dynamics security role that authorizes each selected table and action. Token acquisition, refresh, consent, and role assignment remain IntegrationAccount lifecycle responsibilities. The module does not accept a client secret or refresh token in workflow data.
Configuration
{
"version": "1.0.0",
"authConfig": {
"authStrategy": 1,
"integrationAccount": "integration-account:dynamics-sales-ops"
},
"retryPolicy": {
"maxAttempts": 3,
"backoffStrategy": "EXPONENTIAL",
"initialDelayMs": 1000,
"maxDelayMs": 5000
},
"executionConfig": {"timeoutMs": 30000},
"payloadConfig": {
"parameters": "{\"organizationUrl\":\"https://acme.crm.dynamics.com\",\"operation\":\"list_opportunities\",\"select\":\"opportunityid,name,estimatedvalue,estimatedclosedate\",\"filter\":\"statecode eq 0\",\"orderBy\":\"estimatedclosedate asc\",\"limit\":100}"
}
}
The account reference is symbolic. Persisted workflows bind the generated IntegrationAccount relationship, never plaintext credentials.
Operations
| Operation family | Supported resources | Behavior | Side effect |
|---|---|---|---|
list_<resource> | accounts, contacts, leads, opportunities | Returns one bounded OData page with optional selection, filter, ordering, and continuation. | Read-only; safe retries. |
get_<resource> | account, contact, lead, opportunity | Reads one UUID-addressed row with an optional projection. | Read-only; safe retries. |
create_<resource> | account, contact, lead, opportunity | Creates one row from allowlisted fields and requests a representation. | Mutation; one attempt. |
update_<resource> | account, contact, lead, opportunity | Patches one row, optionally guarded by If-Match. | Mutation; one attempt. |
delete_<resource> | account, contact, lead, opportunity | Deletes or deactivates according to provider behavior after explicit confirmation. | Destructive; one attempt. |
The exact twenty operation names are list_accounts, get_account, create_account, update_account, delete_account, and the equivalent five operations for contacts, leads, and opportunities.
Errors and Failure Modes
| Code | Typical cause | Retryable | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Missing UUID/create field, unsafe tenant, invalid field/JSON/ETag/filter/cursor/bound, empty mutation, or absent delete confirmation. | No | Correct the named input; no unsafe request was sent. |
UNSUPPORTED_OPERATION | Unknown operation or entity family. | No | Select one of the twenty documented operations. |
INTEGRATION_ACCOUNT_REQUIRED | No account is bound. | No | Bind a Microsoft Dynamics IntegrationAccount. |
INTEGRATION_ACCOUNT_NOT_READY | Account is not exactly READY. | No | Repair or reconnect the account. |
DYNAMICS_HTTP_400 | Invalid field, OData expression, relationship binding, or provider constraint. | No | Correct the bounded request using tenant metadata. |
DYNAMICS_HTTP_401 | Access token is missing, expired, revoked, or issued for the wrong resource. | No | Refresh the IntegrationAccount OAuth grant. |
DYNAMICS_HTTP_403 | Dynamics security role lacks table or action privilege. | No | Grant only the missing least-privilege permission. |
DYNAMICS_HTTP_404 | UUID is wrong, deleted, or invisible. | No | Re-run discovery under the same principal. |
DYNAMICS_HTTP_412 | ETag does not match the current row. | No | Read the current row, reconcile changes, and retry intentionally. |
DYNAMICS_HTTP_429 | Dataverse service-protection limit is exhausted. | Yes for reads | Honor Retry-After and reduce concurrency. |
DYNAMICS_HTTP_5xx | Transient provider failure. | Yes for reads | Retry reads; reconcile writes before manual replay. |
NETWORK_ERROR | Timeout, DNS, TLS, or connectivity failure. | Yes for reads | Verify connectivity; inspect Dynamics before repeating a write. |
RESPONSE_TOO_LARGE | Response exceeded 5 MiB. | No | Lower limit or narrow select and filter. |
EXECUTION_ERROR | An unexpected local normalization failure occurred. | No | Inspect sanitized logs and provider compatibility. |
Example
Create an opportunity after a governed workflow validates the account and commercial terms:
{
"operation": "create_opportunity",
"record": {
"name": "Enterprise platform renewal",
"description": "Verified expansion opportunity from Q3 account review.",
"estimatedvalue": 25000,
"estimatedclosedate": "2026-09-30",
"closeprobability": 70,
"customerid_account@odata.bind": "/accounts(11111111-1111-4111-8111-111111111111)"
}
}
Expected result:
{
"status": "success",
"operation": "create_opportunity",
"resource": "opportunities",
"attempts": 1,
"httpStatus": 201,
"id": "33333333-3333-4333-8333-333333333333",
"data": {
"opportunityid": "33333333-3333-4333-8333-333333333333",
"name": "Enterprise platform renewal",
"estimatedvalue": 25000
}
}
Notes
- Pagination: Dataverse list operations request at most 5,000 rows and expose only a validated
$skiptokenfrom a trusted@odata.nextLink. FeednextCursorback ascursor; do not construct or edit it. - Rate limits: HTTP 408, 429, 500, 502, 503, and 504 and network failures may retry for GET operations, honoring numeric
Retry-Afterwithin a five-second local bound. Writes never retry automatically. - API limits: request JSON is capped at 256 KiB, list output at 5,000 rows per page, and every response at 5 MiB. Selection and filtering should be as narrow as the workflow permits.
- Idempotency: reads are repeatable for a fixed provider state. Create, update, and delete are single-attempt because a timeout can follow a committed mutation. Reconcile by UUID or business key before replaying a write.
- Concurrency: pass the current row ETag for update or delete when lost-update protection matters. A 412 is a reconciliation signal, not a retry signal.
- Destructive behavior: every delete requires
confirmDelete=true. Dynamics cascading, auditing, deactivation, retention, recycle-bin, and plugin behavior is tenant-configured; review those rules before enabling delete operations. - API behavior: synchronous Dynamics plugins, Power Automate flows, business rules, duplicate detection, auditing, notifications, and downstream integrations can run as side effects of a mutation.
- Authentication: this release supports global commercial Dynamics hosts matching
*.crmN.dynamics.com. Government and China clouds are deferred until their authority and host contracts are modeled explicitly. - External verification: deterministic local tests cover OAuth headers, trusted routing, account/contact/lead/opportunity paths, bounded OData, pagination, allowlists, UUID identity, ETags, safe-read retry, single-attempt writes, delete confirmation, redaction, and metadata discovery. Live tenant roles, plugins, service-protection quotas, and custom columns require separately authorized Dynamics credentials and are not exercised in repository tests.
- Deferred operations: custom tables/columns, metadata discovery, FetchXML, alternate keys, upsert, batch and change sets, file/image columns, notes and activities, lead qualification, opportunity close/reopen actions, duplicate detection controls, relationship management beyond the exposed bindings, webhooks, and triggers are deferred.
- Functional references: n8n Microsoft Dynamics CRM node and Microsoft Dataverse Web API.