Skip to content

Commit a21900a

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat(integrations): add Dynamics 365 CRM
1 parent 3848f97 commit a21900a

44 files changed

Lines changed: 6767 additions & 41 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/components/ui/icon-mapping.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
429429
memory: BrainIcon,
430430
microsoft_ad: AzureIcon,
431431
microsoft_dataverse: MicrosoftDataverseIcon,
432+
microsoft_dynamics_365: MicrosoftDataverseIcon,
432433
microsoft_excel: MicrosoftExcelIcon,
433434
microsoft_excel_v2: MicrosoftExcelIcon,
434435
microsoft_planner: MicrosoftPlannerIcon,

apps/docs/content/docs/en/integrations/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
"memory",
156156
"microsoft_ad",
157157
"microsoft_dataverse",
158+
"microsoft_dynamics_365",
158159
"microsoft_excel",
159160
"microsoft_planner",
160161
"microsoft_teams",
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
---
2+
title: Microsoft Dynamics 365 CRM
3+
description: Manage customers, sales pipelines, and support cases in Dynamics 365 CRM
4+
---
5+
6+
import { BlockInfoCard } from "@/components/ui/block-info-card"
7+
8+
<BlockInfoCard
9+
type="microsoft_dynamics_365"
10+
color="#FFFFFF"
11+
/>
12+
13+
{/* MANUAL-CONTENT-START:intro */}
14+
[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.
15+
16+
Two block operations intentionally reuse the new CRM tools under a simpler CRM label:
17+
18+
- **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.
19+
- **Assign Record** updates the selected record's `ownerid@odata.bind` to an explicitly chosen user or team.
20+
21+
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.
22+
{/* MANUAL-CONTENT-END */}
23+
24+
## Usage Instructions
25+
26+
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.
27+
28+
29+
30+
## Actions
31+
32+
### List Microsoft Dynamics 365 CRM Records
33+
34+
Query and list records from a Microsoft Dataverse table. Supports OData query options for filtering, selecting columns, ordering, and pagination.
35+
36+
#### Input
37+
38+
| Parameter | Type | Required | Description |
39+
| --------- | ---- | -------- | ----------- |
40+
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
41+
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
42+
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
43+
| `select` | string | No | Comma-separated list of columns to return \(OData $select\) |
44+
| `filter` | string | No | OData $filter expression \(e.g., statecode eq 0\) |
45+
| `orderBy` | string | No | OData $orderby expression \(e.g., name asc, createdon desc\) |
46+
| `pageSize` | number | No | Maximum records in this page \(default and maximum: 100\) |
47+
| `expand` | string | No | Navigation properties to expand \(OData $expand\) |
48+
| `count` | string | No | Set to "true" to include total record count in response \(OData $count\) |
49+
| `nextLink` | string | No | Exact nextLink returned by a previous page of this operation |
50+
51+
#### Output
52+
53+
| Parameter | Type | Description |
54+
| --------- | ---- | ----------- |
55+
| `records` | array | Array of Dataverse records. Each record has dynamic columns based on the table schema. |
56+
| `count` | number | Number of records returned in the current page |
57+
| `totalCount` | number | Provider-reported matching-record count, which Dataverse may cap \(requires $count=true\) |
58+
| `totalCountLimitExceeded` | boolean | Whether Dataverse capped the provider-reported matching-record count |
59+
| `nextLink` | string | URL for the next page of results |
60+
| `success` | boolean | Operation success status |
61+
62+
### Get Microsoft Dynamics 365 CRM Record
63+
64+
Retrieve a single record from a Microsoft Dataverse table by its ID. Supports $select and $expand OData query options.
65+
66+
#### Input
67+
68+
| Parameter | Type | Required | Description |
69+
| --------- | ---- | -------- | ----------- |
70+
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
71+
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
72+
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
73+
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to retrieve |
74+
| `select` | string | No | Comma-separated list of columns to return \(OData $select\) |
75+
| `expand` | string | No | Navigation properties to expand \(OData $expand\) |
76+
77+
#### Output
78+
79+
| Parameter | Type | Description |
80+
| --------- | ---- | ----------- |
81+
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
82+
| `recordId` | string | The requested record ID |
83+
| `success` | boolean | Whether the record was retrieved successfully |
84+
85+
### Create Microsoft Dynamics 365 CRM Record
86+
87+
Create a new record in a Microsoft Dataverse table. Requires the entity set name (plural table name) and record data as a JSON object.
88+
89+
#### Input
90+
91+
| Parameter | Type | Required | Description |
92+
| --------- | ---- | -------- | ----------- |
93+
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
94+
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
95+
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
96+
| `data` | object | Yes | Record data as a JSON object with column names as keys |
97+
98+
#### Output
99+
100+
| Parameter | Type | Description |
101+
| --------- | ---- | ----------- |
102+
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
103+
| `recordId` | string | The ID of the created record |
104+
| `success` | boolean | Whether the record was created successfully |
105+
106+
### Update Microsoft Dynamics 365 CRM Record
107+
108+
Update an existing record in a Microsoft Dataverse table. Only send the columns you want to change.
109+
110+
#### Input
111+
112+
| Parameter | Type | Required | Description |
113+
| --------- | ---- | -------- | ----------- |
114+
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
115+
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
116+
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
117+
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to update |
118+
| `data` | object | Yes | Record data to update as a JSON object with column names as keys |
119+
120+
#### Output
121+
122+
| Parameter | Type | Description |
123+
| --------- | ---- | ----------- |
124+
| `recordId` | string | The ID of the updated record |
125+
| `success` | boolean | Operation success status |
126+
127+
### Search Microsoft Dynamics 365 CRM Records
128+
129+
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.
130+
131+
#### Input
132+
133+
| Parameter | Type | Required | Description |
134+
| --------- | ---- | -------- | ----------- |
135+
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
136+
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
137+
| `searchTerm` | string | Yes | Search text \(1-100 chars\). Supports simple syntax: + \(AND\), \| \(OR\), - \(NOT\), * \(wildcard\), "exact phrase" |
138+
| `entities` | string | No | JSON array of search entity configs. Each object: \{"name":"account","selectColumns":\["name"\],"searchColumns":\["name"\],"filter":"statecode eq 0"\} |
139+
| `filter` | string | No | Global OData filter applied across all entities \(e.g., "createdon gt 2024-01-01"\) |
140+
| `facets` | string | No | JSON array of facet specifications \(e.g., \["entityname,count:100","ownerid,count:100"\]\) |
141+
| `top` | number | No | Maximum number of results \(default: 50, max: 100\) |
142+
| `skip` | number | No | Number of results to skip for pagination |
143+
| `orderBy` | string | No | JSON array of sort expressions \(e.g., \["createdon desc"\]\) |
144+
| `searchMode` | string | No | Search mode: "any" \(default, match any term\) or "all" \(match all terms\) |
145+
| `searchType` | string | No | Query type: "simple" \(default\) or "lucene" \(enables regex, fuzzy, proximity, boosting\) |
146+
147+
#### Output
148+
149+
| Parameter | Type | Description |
150+
| --------- | ---- | ----------- |
151+
| `results` | array | Array of search result objects |
152+
|`Id` | string | Record GUID |
153+
|`EntityName` | string | Table logical name \(e.g., account, contact\) |
154+
|`ObjectTypeCode` | number | Entity type code |
155+
|`Attributes` | object | Record attributes matching the search. Keys are column logical names. |
156+
|`Highlights` | object | Highlighted search matches. Keys are column names, values are arrays of strings with \{crmhit\}/\{/crmhit\} markers. |
157+
|`Score` | number | Relevance score for this result |
158+
| `totalCount` | number | Total number of matching records across all tables |
159+
| `count` | number | Number of results returned in this page |
160+
| `facets` | object | Facet results when facets were requested. Keys are facet names, values are arrays of facet value objects with count and value properties. |
161+
| `success` | boolean | Operation success status |
162+
163+
### Qualify Microsoft Dynamics 365 Lead
164+
165+
Qualify a Dynamics 365 Sales lead and optionally create linked account, contact, and opportunity records.
166+
167+
#### Input
168+
169+
| Parameter | Type | Required | Description |
170+
| --------- | ---- | -------- | ----------- |
171+
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
172+
| `environmentUrl` | string | Yes | Dynamics 365 environment URL \(e.g., https://myorg.crm.dynamics.com\) |
173+
| `leadId` | string | Yes | GUID of the lead to qualify |
174+
| `createAccount` | boolean | Yes | Whether to create an account from the lead |
175+
| `createContact` | boolean | Yes | Whether to create a contact from the lead |
176+
| `createOpportunity` | boolean | Yes | Whether to create an opportunity from the lead |
177+
| `statusReason` | number | No | Qualified lead status-reason value \(default: 3\) |
178+
| `opportunityCurrencyId` | string | No | Optional transaction currency GUID for the created opportunity |
179+
| `opportunityCustomerId` | string | No | Optional account or contact GUID for the created opportunity customer |
180+
| `opportunityCustomerType` | string | No | Customer table type for opportunityCustomerId: account or contact |
181+
| `sourceCampaignId` | string | No | Optional source campaign GUID for the created opportunity |
182+
| `processInstanceId` | string | No | Optional business process flow instance GUID for the created opportunity |
183+
| `processInstanceEntityType` | string | No | Logical table name for the business process flow instance |
184+
185+
#### Output
186+
187+
| Parameter | Type | Description |
188+
| --------- | ---- | ----------- |
189+
| `createdEntities` | array | Entity references returned by Dataverse for records created while qualifying the lead |
190+
| `success` | boolean | Whether the lead was qualified successfully |
191+
192+
### Close Microsoft Dynamics 365 Opportunity
193+
194+
Close a Dynamics 365 Sales opportunity as won or lost.
195+
196+
#### Input
197+
198+
| Parameter | Type | Required | Description |
199+
| --------- | ---- | -------- | ----------- |
200+
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
201+
| `environmentUrl` | string | Yes | Dynamics 365 environment URL \(e.g., https://myorg.crm.dynamics.com\) |
202+
| `opportunityId` | string | Yes | GUID of the opportunity to close |
203+
| `outcome` | string | Yes | Opportunity outcome: won or lost |
204+
| `subject` | string | Yes | Subject for the opportunity-close activity \(maximum 200 characters\) |
205+
| `description` | string | No | Optional description for the opportunity-close activity |
206+
| `statusReason` | number | No | Opportunity status-reason value \(defaults to 3 for won or 4 for lost\) |
207+
208+
#### Output
209+
210+
| Parameter | Type | Description |
211+
| --------- | ---- | ----------- |
212+
| `opportunityId` | string | GUID of the closed opportunity |
213+
| `outcome` | string | The applied opportunity outcome: won or lost |
214+
| `success` | boolean | Whether the opportunity was closed successfully |
215+
216+
### Close Microsoft Dynamics 365 Case
217+
218+
Resolve and close a Dynamics 365 Customer Service case.
219+
220+
#### Input
221+
222+
| Parameter | Type | Required | Description |
223+
| --------- | ---- | -------- | ----------- |
224+
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
225+
| `environmentUrl` | string | Yes | Dynamics 365 environment URL \(e.g., https://myorg.crm.dynamics.com\) |
226+
| `caseId` | string | Yes | GUID of the case to close |
227+
| `subject` | string | Yes | Subject for the case-resolution activity \(maximum 200 characters\) |
228+
| `description` | string | No | Optional description for the case-resolution activity |
229+
| `timeSpent` | number | No | Optional nonnegative number of minutes spent resolving the case |
230+
| `statusReason` | number | No | Resolved case status-reason value \(default: 5\) |
231+
232+
#### Output
233+
234+
| Parameter | Type | Description |
235+
| --------- | ---- | ----------- |
236+
| `caseId` | string | GUID of the closed case |
237+
| `success` | boolean | Whether the case was closed successfully |

apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ import {
2626
parseProvider,
2727
} from '@/lib/oauth'
2828
import { getScopeDescription, getServiceConfigByProviderId } from '@/lib/oauth/utils'
29+
import {
30+
MicrosoftDataverseEnvironmentField,
31+
useMicrosoftDataverseEnvironmentForm,
32+
} from '@/app/workspace/[workspaceId]/components/connect-oauth-modal/microsoft-dataverse-environment'
2933
import { useCreateCredentialDraft, useWorkspaceCredentials } from '@/hooks/queries/credentials'
34+
import {
35+
assertMicrosoftDataverseWebOAuthAvailable,
36+
useConnectMicrosoftDataverseOAuthService,
37+
} from '@/hooks/queries/oauth/microsoft-dataverse-connections'
3038
import { useConnectOAuthService } from '@/hooks/queries/oauth/oauth-connections'
3139

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

89101
/**
@@ -156,6 +168,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
156168

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

160173
const [displayName, setDisplayName] = useState('')
161174
const [description, setDescription] = useState('')
@@ -180,6 +193,14 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
180193
})
181194
const createDraft = useCreateCredentialDraft()
182195
const connectOAuthService = useConnectOAuthService()
196+
const connectMicrosoftDataverseOAuthService = useConnectMicrosoftDataverseOAuthService()
197+
const dataverseEnvironmentForm = useMicrosoftDataverseEnvironmentForm({
198+
fallbackScopes: requiredScopes,
199+
lockedEnvironmentUrl: props.dataverseEnvironmentUrl,
200+
open,
201+
providerId,
202+
required: props.requireDataverseEnvironment === true,
203+
})
183204

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

198-
const requiredScopes = props.requiredScopes ?? EMPTY_SCOPES
199219
const newScopes = !isConnect ? (props.newScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES
200220

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

206226
const displayScopes = useMemo(() => {
207-
const filtered = [...requiredScopes].filter((scope) => !isHiddenScope(scope))
227+
const filtered = [...dataverseEnvironmentForm.effectiveScopes].filter(
228+
(scope) => !isHiddenScope(scope)
229+
)
208230
if (isConnect) return filtered
209231
return filtered.sort((a, b) => {
210232
const aIsNew = newScopesSet.has(a)
@@ -213,7 +235,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
213235
if (!aIsNew && bIsNew) return 1
214236
return 0
215237
})
216-
}, [isConnect, requiredScopes, newScopesSet])
238+
}, [isConnect, dataverseEnvironmentForm.effectiveScopes, newScopesSet])
217239

218240
/**
219241
* Initialize the connect form once per open session, after credentials have
@@ -255,6 +277,10 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
255277
setValidationError(null)
256278
setSubmitError(null)
257279
try {
280+
const environmentUrl = dataverseEnvironmentForm.validate()
281+
if (dataverseEnvironmentForm.enabled && !environmentUrl) return
282+
if (environmentUrl) assertMicrosoftDataverseWebOAuthAvailable()
283+
258284
let connectorType: string | undefined
259285

260286
if (isConnect) {
@@ -316,10 +342,17 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
316342
callbackURL.searchParams.set(ADD_CONNECTOR_SEARCH_PARAM, connectorType)
317343
}
318344

319-
await connectOAuthService.mutateAsync({
320-
providerId,
321-
callbackURL: callbackURL.toString(),
322-
})
345+
if (environmentUrl) {
346+
await connectMicrosoftDataverseOAuthService.mutateAsync({
347+
callbackURL: callbackURL.toString(),
348+
environmentUrl,
349+
})
350+
} else {
351+
await connectOAuthService.mutateAsync({
352+
providerId,
353+
callbackURL: callbackURL.toString(),
354+
})
355+
}
323356
handleClose()
324357
} catch (err: unknown) {
325358
const message = getErrorMessage(err, 'Failed to start OAuth connection')
@@ -328,10 +361,16 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
328361
}
329362
}
330363

331-
const isPending = (isConnect && createDraft.isPending) || connectOAuthService.isPending
364+
const isPending =
365+
(isConnect && createDraft.isPending) ||
366+
connectOAuthService.isPending ||
367+
connectMicrosoftDataverseOAuthService.isPending
332368
const isDisabled = isConnect
333-
? !displayName.trim() || isPending || Boolean(existingCredential)
334-
: isPending
369+
? !displayName.trim() ||
370+
!dataverseEnvironmentForm.isComplete ||
371+
isPending ||
372+
Boolean(existingCredential)
373+
: !dataverseEnvironmentForm.isComplete || isPending
335374

336375
const displayNameError =
337376
validationError ??
@@ -381,6 +420,8 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
381420
/>
382421
)}
383422

423+
<MicrosoftDataverseEnvironmentField form={dataverseEnvironmentForm} />
424+
384425
{isConnect && (
385426
<ChipModalField
386427
type='textarea'

0 commit comments

Comments
 (0)