Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
memory: BrainIcon,
microsoft_ad: AzureIcon,
microsoft_dataverse: MicrosoftDataverseIcon,
microsoft_dynamics_365: MicrosoftDataverseIcon,
microsoft_excel: MicrosoftExcelIcon,
microsoft_excel_v2: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/en/integrations/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"memory",
"microsoft_ad",
"microsoft_dataverse",
"microsoft_dynamics_365",
"microsoft_excel",
"microsoft_planner",
"microsoft_teams",
Expand Down
239 changes: 239 additions & 0 deletions apps/docs/content/docs/en/integrations/microsoft_dynamics_365.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
---
title: Microsoft Dynamics 365 CRM
description: Manage customers, sales pipelines, and support cases in Dynamics 365 CRM
---

import { BlockInfoCard } from "@/components/ui/block-info-card"

<BlockInfoCard
type="microsoft_dynamics_365"
color="#FFFFFF"
/>

{/* MANUAL-CONTENT-START:intro */}
[Microsoft Dynamics 365 CRM](https://www.microsoft.com/en-us/dynamics-365) stores its sales and customer-service data in Microsoft Dataverse. This integration presents the standard CRM tables and lifecycle actions while keeping the generic Microsoft Dataverse integration available for custom tables and advanced operations.

Two block operations intentionally reuse the new CRM tools under a simpler CRM label:

- **List Owners** lists active users or owner-capable teams. The results are candidates; Dynamics security roles and table privileges still determine whether a particular user or team can own a record.
- **Assign Record** updates the selected record's `ownerid@odata.bind` to an explicitly chosen user or team.

Connect one credential per Dynamics environment. The credential is bound to that environment during Microsoft OAuth, and CRM requests refuse a different environment. This release supports public-cloud Dataverse hosts only.
{/* MANUAL-CONTENT-END */}

## Usage Instructions

Manage standard Microsoft Dynamics 365 CRM records through the Dataverse Web API. List, search, create, retrieve, and update accounts, contacts, leads, opportunities, and cases; assign records to users or teams; qualify leads; close opportunities; and resolve cases. Connect a separate Microsoft credential for each environment from this Dynamics integration page or from its workflow block; existing generic Dataverse credentials remain unchanged and are not automatically rebound. This version supports public-cloud Dynamics environments; national clouds require separate OAuth authorities. Dataverse Search must be enabled for search, and lifecycle actions require the corresponding Dynamics 365 app, security role, and record privileges.



## Actions

### List Microsoft Dynamics 365 CRM Records

Query and list records from a Microsoft Dataverse table. Supports OData query options for filtering, selecting columns, ordering, and pagination.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
| `select` | string | No | Comma-separated list of columns to return \(OData $select\) |
| `filter` | string | No | OData $filter expression \(e.g., statecode eq 0\) |
| `orderBy` | string | No | OData $orderby expression \(e.g., name asc, createdon desc\) |
| `pageSize` | number | No | Maximum records in this page \(default and maximum: 100\) |
| `expand` | string | No | Navigation properties to expand \(OData $expand\) |
| `count` | string | No | Set to "true" to include total record count in response \(OData $count\) |
| `nextLink` | string | No | Exact nextLink returned by a previous page of this operation |
| `nextPageSize` | number | No | Exact nextPageSize returned alongside nextLink by the previous page |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | array | Array of Dataverse records. Each record has dynamic columns based on the table schema. |
| `count` | number | Number of records returned in the current page |
| `totalCount` | number | Provider-reported matching-record count, which Dataverse may cap \(requires $count=true\) |
| `totalCountLimitExceeded` | boolean | Whether Dataverse capped the provider-reported matching-record count |
| `nextLink` | string | URL for the next page of results |
| `nextPageSize` | number | Page size that must accompany nextLink on the continuation request |
| `success` | boolean | Operation success status |

### Get Microsoft Dynamics 365 CRM Record

Retrieve a single record from a Microsoft Dataverse table by its ID. Supports $select and $expand OData query options.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to retrieve |
| `select` | string | No | Comma-separated list of columns to return \(OData $select\) |
| `expand` | string | No | Navigation properties to expand \(OData $expand\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
| `recordId` | string | The requested record ID |
| `success` | boolean | Whether the record was retrieved successfully |

### Create Microsoft Dynamics 365 CRM Record

Create a new record in a Microsoft Dataverse table. Requires the entity set name (plural table name) and record data as a JSON object.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
| `data` | object | Yes | Record data as a JSON object with column names as keys |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
| `recordId` | string | The ID of the created record |
| `success` | boolean | Whether the record was created successfully |

### Update Microsoft Dynamics 365 CRM Record

Update an existing record in a Microsoft Dataverse table. Only send the columns you want to change.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to update |
| `data` | object | Yes | Record data to update as a JSON object with column names as keys |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `recordId` | string | The ID of the updated record |
| `success` | boolean | Operation success status |

### Search Microsoft Dynamics 365 CRM Records

Perform a full-text relevance search across Microsoft Dataverse tables. Requires Dataverse Search to be enabled on the environment. Supports simple and Lucene query syntax.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
| `searchTerm` | string | Yes | Search text \(1-100 chars\). Supports simple syntax: + \(AND\), \| \(OR\), - \(NOT\), * \(wildcard\), "exact phrase" |
| `entities` | string | No | JSON array of search entity configs. Each object: \{"name":"account","selectColumns":\["name"\],"searchColumns":\["name"\],"filter":"statecode eq 0"\} |
| `filter` | string | No | Global OData filter applied across all entities \(e.g., "createdon gt 2024-01-01"\) |
| `facets` | string | No | JSON array of facet specifications \(e.g., \["entityname,count:100","ownerid,count:100"\]\) |
| `top` | number | No | Maximum number of results \(default: 50, max: 100\) |
| `skip` | number | No | Number of results to skip for pagination |
| `orderBy` | string | No | JSON array of sort expressions \(e.g., \["createdon desc"\]\) |
| `searchMode` | string | No | Search mode: "any" \(default, match any term\) or "all" \(match all terms\) |
| `searchType` | string | No | Query type: "simple" \(default\) or "lucene" \(enables regex, fuzzy, proximity, boosting\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | array | Array of search result objects |
| ↳ `Id` | string | Record GUID |
| ↳ `EntityName` | string | Table logical name \(e.g., account, contact\) |
| ↳ `ObjectTypeCode` | number | Entity type code |
| ↳ `Attributes` | object | Record attributes matching the search. Keys are column logical names. |
| ↳ `Highlights` | object | Highlighted search matches. Keys are column names, values are arrays of strings with \{crmhit\}/\{/crmhit\} markers. |
| ↳ `Score` | number | Relevance score for this result |
| `totalCount` | number | Total number of matching records across all tables |
| `count` | number | Number of results returned in this page |
| `facets` | object | Facet results when facets were requested. Keys are facet names, values are arrays of facet value objects with count and value properties. |
| `success` | boolean | Operation success status |

### Qualify Microsoft Dynamics 365 Lead

Qualify a Dynamics 365 Sales lead and optionally create linked account, contact, and opportunity records.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
| `environmentUrl` | string | Yes | Dynamics 365 environment URL \(e.g., https://myorg.crm.dynamics.com\) |
| `leadId` | string | Yes | GUID of the lead to qualify |
| `createAccount` | boolean | Yes | Whether to create an account from the lead |
| `createContact` | boolean | Yes | Whether to create a contact from the lead |
| `createOpportunity` | boolean | Yes | Whether to create an opportunity from the lead |
| `statusReason` | number | No | Qualified lead status-reason value \(default: 3\) |
| `opportunityCurrencyId` | string | No | Optional transaction currency GUID for the created opportunity |
| `opportunityCustomerId` | string | No | Optional account or contact GUID for the created opportunity customer |
| `opportunityCustomerType` | string | No | Customer table type for opportunityCustomerId: account or contact |
| `sourceCampaignId` | string | No | Optional source campaign GUID for the created opportunity |
| `processInstanceId` | string | No | Optional business process flow instance GUID for the created opportunity |
| `processInstanceEntityType` | string | No | Logical table name for the business process flow instance |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `createdEntities` | array | Entity references returned by Dataverse for records created while qualifying the lead |
| `success` | boolean | Whether the lead was qualified successfully |

### Close Microsoft Dynamics 365 Opportunity

Close a Dynamics 365 Sales opportunity as won or lost.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
| `environmentUrl` | string | Yes | Dynamics 365 environment URL \(e.g., https://myorg.crm.dynamics.com\) |
| `opportunityId` | string | Yes | GUID of the opportunity to close |
| `outcome` | string | Yes | Opportunity outcome: won or lost |
| `subject` | string | No | Optional subject for the opportunity-close activity \(maximum 200 characters\) |
| `description` | string | No | Optional description for the opportunity-close activity \(maximum 2,000 characters\) |
| `statusReason` | number | No | Opportunity status-reason value \(defaults to 3 for won or 4 for lost\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `opportunityId` | string | GUID of the closed opportunity |
| `outcome` | string | The applied opportunity outcome: won or lost |
| `success` | boolean | Whether the opportunity was closed successfully |

### Close Microsoft Dynamics 365 Case

Resolve and close a Dynamics 365 Customer Service case.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
| `environmentUrl` | string | Yes | Dynamics 365 environment URL \(e.g., https://myorg.crm.dynamics.com\) |
| `caseId` | string | Yes | GUID of the case to close |
| `subject` | string | Yes | Subject for the case-resolution activity \(maximum 200 characters\) |
| `description` | string | No | Optional description for the case-resolution activity \(maximum 100,000 characters\) |
| `timeSpent` | number | No | Optional nonnegative number of minutes spent resolving the case |
| `statusReason` | number | No | Resolved case status-reason value \(default: 5\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `caseId` | string | GUID of the closed case |
| `success` | boolean | Whether the case was closed successfully |
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ import {
parseProvider,
} from '@/lib/oauth'
import { getScopeDescription, getServiceConfigByProviderId } from '@/lib/oauth/utils'
import {
MicrosoftDataverseEnvironmentField,
useMicrosoftDataverseEnvironmentForm,
} from '@/app/workspace/[workspaceId]/components/connect-oauth-modal/microsoft-dataverse-environment'
import { useCreateCredentialDraft, useWorkspaceCredentials } from '@/hooks/queries/credentials'
import {
assertMicrosoftDataverseWebOAuthAvailable,
useConnectMicrosoftDataverseOAuthService,
} from '@/hooks/queries/oauth/microsoft-dataverse-connections'
import { useConnectOAuthService } from '@/hooks/queries/oauth/oauth-connections'

const logger = createLogger('ConnectOAuthModal')
Expand Down Expand Up @@ -84,6 +92,10 @@ interface ConnectOAuthModalBaseProps {
/** Used to resolve display metadata and the provider id when not supplied directly. */
provider?: OAuthProvider
serviceId?: string
/** Enables the environment-bound Dynamics 365 OAuth flow. Legacy Dataverse callers omit it. */
requireDataverseEnvironment?: boolean
/** Locks an environment-bound connection to the workflow or credential's selected environment. */
dataverseEnvironmentUrl?: string
}

/**
Expand Down Expand Up @@ -156,6 +168,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {

const [selectedProviderId, setSelectedProviderId] = useState<string | null>(null)
const providerId = selectedProviderId ?? declaredProviderId
const requiredScopes = props.requiredScopes ?? EMPTY_SCOPES

const [displayName, setDisplayName] = useState('')
const [description, setDescription] = useState('')
Expand All @@ -180,6 +193,14 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
})
const createDraft = useCreateCredentialDraft()
const connectOAuthService = useConnectOAuthService()
const connectMicrosoftDataverseOAuthService = useConnectMicrosoftDataverseOAuthService()
const dataverseEnvironmentForm = useMicrosoftDataverseEnvironmentForm({
fallbackScopes: requiredScopes,
lockedEnvironmentUrl: props.dataverseEnvironmentUrl,
open,
providerId,
required: props.requireDataverseEnvironment === true,
})

/**
* Lowercased set of OAuth credential names already in the workspace. Drives
Expand All @@ -195,7 +216,6 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
[credentials]
)

const requiredScopes = props.requiredScopes ?? EMPTY_SCOPES
const newScopes = !isConnect ? (props.newScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES

const newScopesSet = useMemo(
Expand All @@ -204,7 +224,9 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
)

const displayScopes = useMemo(() => {
const filtered = [...requiredScopes].filter((scope) => !isHiddenScope(scope))
const filtered = [...dataverseEnvironmentForm.effectiveScopes].filter(
(scope) => !isHiddenScope(scope)
)
if (isConnect) return filtered
return filtered.sort((a, b) => {
const aIsNew = newScopesSet.has(a)
Expand All @@ -213,7 +235,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
if (!aIsNew && bIsNew) return 1
return 0
})
}, [isConnect, requiredScopes, newScopesSet])
}, [isConnect, dataverseEnvironmentForm.effectiveScopes, newScopesSet])

/**
* Initialize the connect form once per open session, after credentials have
Expand Down Expand Up @@ -255,6 +277,10 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
setValidationError(null)
setSubmitError(null)
try {
const environmentUrl = dataverseEnvironmentForm.validate()
if (dataverseEnvironmentForm.enabled && !environmentUrl) return
if (environmentUrl) assertMicrosoftDataverseWebOAuthAvailable()

let connectorType: string | undefined

if (isConnect) {
Expand Down Expand Up @@ -316,10 +342,17 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
callbackURL.searchParams.set(ADD_CONNECTOR_SEARCH_PARAM, connectorType)
}

await connectOAuthService.mutateAsync({
providerId,
callbackURL: callbackURL.toString(),
})
if (environmentUrl) {
await connectMicrosoftDataverseOAuthService.mutateAsync({
callbackURL: callbackURL.toString(),
environmentUrl,
})
} else {
await connectOAuthService.mutateAsync({
providerId,
callbackURL: callbackURL.toString(),
})
}
handleClose()
} catch (err: unknown) {
const message = getErrorMessage(err, 'Failed to start OAuth connection')
Expand All @@ -328,10 +361,16 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
}
}

const isPending = (isConnect && createDraft.isPending) || connectOAuthService.isPending
const isPending =
(isConnect && createDraft.isPending) ||
connectOAuthService.isPending ||
connectMicrosoftDataverseOAuthService.isPending
const isDisabled = isConnect
? !displayName.trim() || isPending || Boolean(existingCredential)
: isPending
? !displayName.trim() ||
!dataverseEnvironmentForm.isComplete ||
isPending ||
Boolean(existingCredential)
: !dataverseEnvironmentForm.isComplete || isPending

const displayNameError =
validationError ??
Expand Down Expand Up @@ -381,6 +420,8 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
/>
)}

<MicrosoftDataverseEnvironmentField form={dataverseEnvironmentForm} />

{isConnect && (
<ChipModalField
type='textarea'
Expand Down
Loading
Loading