Skip to content

Commit 8164dc3

Browse files
committed
fix(integrations): validate Dynamics 365 contracts
1 parent 3615dbc commit 8164dc3

16 files changed

Lines changed: 123 additions & 48 deletions

File tree

apps/docs/content/docs/en/integrations/microsoft_dynamics_365.mdx

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Manage customers, sales pipelines, and support cases in Dynamics 36
55

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

8-
<BlockInfoCard
8+
<BlockInfoCard
99
type="microsoft_dynamics_365"
1010
color="#FFFFFF"
1111
/>
@@ -21,6 +21,7 @@ Two block operations intentionally reuse the new CRM tools under a simpler CRM l
2121
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.
2222
{/* MANUAL-CONTENT-END */}
2323

24+
2425
## Usage Instructions
2526

2627
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.
@@ -31,13 +32,12 @@ Manage standard Microsoft Dynamics 365 CRM records through the Dataverse Web API
3132

3233
### List Microsoft Dynamics 365 CRM Records
3334

34-
Query and list records from a Microsoft Dataverse table. Supports OData query options for filtering, selecting columns, ordering, and pagination.
35+
Query supported standard Microsoft Dynamics 365 CRM records. Supports OData filtering, column selection, ordering, and one bounded page at a time.
3536

3637
#### Input
3738

3839
| Parameter | Type | Required | Description |
3940
| --------- | ---- | -------- | ----------- |
40-
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
4141
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
4242
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
4343
| `select` | string | No | Comma-separated list of columns to return \(OData $select\) |
@@ -63,13 +63,12 @@ Query and list records from a Microsoft Dataverse table. Supports OData query op
6363

6464
### Get Microsoft Dynamics 365 CRM Record
6565

66-
Retrieve a single record from a Microsoft Dataverse table by its ID. Supports $select and $expand OData query options.
66+
Retrieve one supported standard Microsoft Dynamics 365 CRM record by its ID. Supports $select and $expand OData query options.
6767

6868
#### Input
6969

7070
| Parameter | Type | Required | Description |
7171
| --------- | ---- | -------- | ----------- |
72-
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
7372
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
7473
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
7574
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to retrieve |
@@ -86,13 +85,12 @@ Retrieve a single record from a Microsoft Dataverse table by its ID. Supports $s
8685

8786
### Create Microsoft Dynamics 365 CRM Record
8887

89-
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+
Create a new supported standard Microsoft Dynamics 365 CRM record using Dataverse logical column names.
9089

9190
#### Input
9291

9392
| Parameter | Type | Required | Description |
9493
| --------- | ---- | -------- | ----------- |
95-
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
9694
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
9795
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
9896
| `data` | object | Yes | Record data as a JSON object with column names as keys |
@@ -107,13 +105,12 @@ Create a new record in a Microsoft Dataverse table. Requires the entity set name
107105

108106
### Update Microsoft Dynamics 365 CRM Record
109107

110-
Update an existing record in a Microsoft Dataverse table. Only send the columns you want to change.
108+
Update an existing supported standard Microsoft Dynamics 365 CRM record. Only send the columns you want to change.
111109

112110
#### Input
113111

114112
| Parameter | Type | Required | Description |
115113
| --------- | ---- | -------- | ----------- |
116-
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
117114
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
118115
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
119116
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to update |
@@ -134,7 +131,6 @@ Perform a full-text relevance search across Microsoft Dataverse tables. Requires
134131

135132
| Parameter | Type | Required | Description |
136133
| --------- | ---- | -------- | ----------- |
137-
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
138134
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
139135
| `searchTerm` | string | Yes | Search text \(1-100 chars\). Supports simple syntax: + \(AND\), \| \(OR\), - \(NOT\), * \(wildcard\), "exact phrase" |
140136
| `entities` | string | No | JSON array of search entity configs. Each object: \{"name":"account","selectColumns":\["name"\],"searchColumns":\["name"\],"filter":"statecode eq 0"\} |
@@ -170,7 +166,6 @@ Qualify a Dynamics 365 Sales lead and optionally create linked account, contact,
170166

171167
| Parameter | Type | Required | Description |
172168
| --------- | ---- | -------- | ----------- |
173-
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
174169
| `environmentUrl` | string | Yes | Dynamics 365 environment URL \(e.g., https://myorg.crm.dynamics.com\) |
175170
| `leadId` | string | Yes | GUID of the lead to qualify |
176171
| `createAccount` | boolean | Yes | Whether to create an account from the lead |
@@ -199,7 +194,6 @@ Close a Dynamics 365 Sales opportunity as won or lost.
199194

200195
| Parameter | Type | Required | Description |
201196
| --------- | ---- | -------- | ----------- |
202-
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
203197
| `environmentUrl` | string | Yes | Dynamics 365 environment URL \(e.g., https://myorg.crm.dynamics.com\) |
204198
| `opportunityId` | string | Yes | GUID of the opportunity to close |
205199
| `outcome` | string | Yes | Opportunity outcome: won or lost |
@@ -223,7 +217,6 @@ Resolve and close a Dynamics 365 Customer Service case.
223217

224218
| Parameter | Type | Required | Description |
225219
| --------- | ---- | -------- | ----------- |
226-
| `instanceUrl` | string | Yes | Trusted Dynamics 365 environment bound to the selected OAuth credential |
227220
| `environmentUrl` | string | Yes | Dynamics 365 environment URL \(e.g., https://myorg.crm.dynamics.com\) |
228221
| `caseId` | string | Yes | GUID of the case to close |
229222
| `subject` | string | Yes | Subject for the case-resolution activity \(maximum 200 characters\) |
@@ -237,3 +230,5 @@ Resolve and close a Dynamics 365 Customer Service case.
237230
| --------- | ---- | ----------- |
238231
| `caseId` | string | GUID of the closed case |
239232
| `success` | boolean | Whether the case was closed successfully |
233+
234+

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ import {
2626
parseProvider,
2727
} from '@/lib/oauth'
2828
import { getScopeDescription, getServiceConfigByProviderId } from '@/lib/oauth/utils'
29-
import { withBrandIcon } from '@/blocks/brand-icon'
3029
import {
3130
MicrosoftDataverseEnvironmentField,
3231
useMicrosoftDataverseEnvironmentForm,
3332
} from '@/app/workspace/[workspaceId]/components/connect-oauth-modal/microsoft-dataverse-environment'
33+
import { withBrandIcon } from '@/blocks/brand-icon'
3434
import { useCreateCredentialDraft, useWorkspaceCredentials } from '@/hooks/queries/credentials'
3535
import {
3636
assertMicrosoftDataverseWebOAuthAvailable,

apps/sim/blocks/blocks/microsoft_dynamics_365.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,16 @@ describe('MicrosoftDynamics365Block', () => {
243243
recordType: 'account',
244244
searchTerm: 'Contoso',
245245
searchSkip: '0',
246+
searchFilter: ' statecode eq 0 ',
247+
searchFacets: ' ["ownerid,count:100"] ',
248+
searchOrderBy: ' ["createdon desc"] ',
246249
})
247-
).toMatchObject({ skip: 0 })
250+
).toMatchObject({
251+
skip: 0,
252+
filter: 'statecode eq 0',
253+
facets: '["ownerid,count:100"]',
254+
orderBy: '["createdon desc"]',
255+
})
248256
expect(() =>
249257
mapParams({
250258
operation: 'search_records',

apps/sim/blocks/blocks/microsoft_dynamics_365.ts

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ export const MicrosoftDynamics365Block: BlockConfig<DataverseResponse> = {
299299
wandConfig: {
300300
enabled: true,
301301
prompt:
302-
'Generate a JSON object for a Dynamics 365 Dataverse record. Use logical column names, preserve any custom column names supplied by the user, and return only valid JSON.',
302+
'Generate a JSON object for a Dynamics 365 Dataverse record. Use logical column names and preserve any custom column names supplied by the user. Return ONLY valid JSON - no explanations, no extra text.',
303303
placeholder: 'Describe the CRM fields to create or update...',
304304
generationType: 'json-object',
305305
},
@@ -314,7 +314,7 @@ export const MicrosoftDynamics365Block: BlockConfig<DataverseResponse> = {
314314
wandConfig: {
315315
enabled: true,
316316
prompt:
317-
'Generate a comma-separated list of Dynamics 365 Dataverse logical column names. Return only the comma-separated names.',
317+
'Generate a comma-separated list of Dynamics 365 Dataverse logical column names, for example name,telephone1,emailaddress1. Return ONLY the comma-separated names - no explanations, no extra text.',
318318
placeholder: 'Describe the columns to retrieve...',
319319
generationType: 'odata-expression',
320320
},
@@ -329,7 +329,7 @@ export const MicrosoftDynamics365Block: BlockConfig<DataverseResponse> = {
329329
wandConfig: {
330330
enabled: true,
331331
prompt:
332-
'Generate a Dataverse OData $filter expression using logical column names. Return only the expression without the $filter= prefix.',
332+
"Generate a Dataverse OData $filter expression using logical column names, for example statecode eq 0 and contains(name,'Contoso'). Return ONLY the expression without the $filter= prefix - no explanations, no extra text.",
333333
placeholder: 'Describe which CRM records to include...',
334334
generationType: 'odata-expression',
335335
},
@@ -344,7 +344,7 @@ export const MicrosoftDynamics365Block: BlockConfig<DataverseResponse> = {
344344
wandConfig: {
345345
enabled: true,
346346
prompt:
347-
'Generate a Dataverse OData $orderby expression using logical column names. Return only the expression without the $orderby= prefix.',
347+
'Generate a Dataverse OData $orderby expression using logical column names, for example createdon desc,name asc. Return ONLY the expression without the $orderby= prefix - no explanations, no extra text.',
348348
placeholder: 'Describe how to sort the records...',
349349
generationType: 'odata-expression',
350350
},
@@ -359,7 +359,7 @@ export const MicrosoftDynamics365Block: BlockConfig<DataverseResponse> = {
359359
wandConfig: {
360360
enabled: true,
361361
prompt:
362-
'Generate a Dataverse OData $expand expression for the requested relationships. Return only the expression without the $expand= prefix.',
362+
'Generate a Dataverse OData $expand expression for the requested relationships, for example primarycontactid($select=fullname,emailaddress1). Return ONLY the expression without the $expand= prefix - no explanations, no extra text.',
363363
placeholder: 'Describe which related records to include...',
364364
generationType: 'odata-expression',
365365
},
@@ -424,6 +424,54 @@ export const MicrosoftDynamics365Block: BlockConfig<DataverseResponse> = {
424424
condition: { field: 'operation', value: 'search_records' },
425425
mode: 'advanced',
426426
},
427+
{
428+
id: 'searchFilter',
429+
title: 'Search Filter',
430+
type: 'short-input',
431+
placeholder: 'statecode eq 0',
432+
description: 'Global OData filter applied to the selected record type.',
433+
condition: { field: 'operation', value: 'search_records' },
434+
mode: 'advanced',
435+
wandConfig: {
436+
enabled: true,
437+
prompt:
438+
'Generate a Dataverse Search OData filter using logical column names, for example statecode eq 0 and createdon ge 2024-01-01. Return ONLY the expression - no explanations, no extra text.',
439+
placeholder: 'Describe which search results to include...',
440+
generationType: 'odata-expression',
441+
},
442+
},
443+
{
444+
id: 'searchFacets',
445+
title: 'Search Facets',
446+
type: 'long-input',
447+
placeholder: '["entityname,count:100","ownerid,count:100"]',
448+
description: 'JSON array of Dataverse Search facet specifications.',
449+
condition: { field: 'operation', value: 'search_records' },
450+
mode: 'advanced',
451+
wandConfig: {
452+
enabled: true,
453+
prompt:
454+
'Generate a JSON array of Dataverse Search facet specifications, for example ["entityname,count:100","ownerid,count:100"]. Return ONLY the JSON array - no explanations, no extra text.',
455+
placeholder: 'Describe how to group the search results...',
456+
generationType: 'json-array',
457+
},
458+
},
459+
{
460+
id: 'searchOrderBy',
461+
title: 'Search Order By',
462+
type: 'short-input',
463+
placeholder: '["createdon desc"]',
464+
description: 'JSON array of Dataverse Search sort expressions.',
465+
condition: { field: 'operation', value: 'search_records' },
466+
mode: 'advanced',
467+
wandConfig: {
468+
enabled: true,
469+
prompt:
470+
'Generate a JSON array of Dataverse Search sort expressions using logical column names, for example ["createdon desc"]. Return ONLY the JSON array - no explanations, no extra text.',
471+
placeholder: 'Describe how to sort the search results...',
472+
generationType: 'json-array',
473+
},
474+
},
427475
{
428476
id: 'searchSkip',
429477
title: 'Search Offset',
@@ -791,6 +839,15 @@ export const MicrosoftDynamics365Block: BlockConfig<DataverseResponse> = {
791839
entities: JSON.stringify([{ name: recordType.logicalName }]),
792840
top,
793841
...(skip !== undefined && { skip }),
842+
...(optionalString(params.searchFilter) && {
843+
filter: optionalString(params.searchFilter),
844+
}),
845+
...(optionalString(params.searchFacets) && {
846+
facets: optionalString(params.searchFacets),
847+
}),
848+
...(optionalString(params.searchOrderBy) && {
849+
orderBy: optionalString(params.searchOrderBy),
850+
}),
794851
searchMode: requiredString(params.searchMode ?? 'any', 'Search mode'),
795852
searchType: requiredString(params.searchType ?? 'simple', 'Query type'),
796853
}
@@ -958,6 +1015,12 @@ export const MicrosoftDynamics365Block: BlockConfig<DataverseResponse> = {
9581015
},
9591016
searchTerm: { type: 'string', description: 'Dataverse Search query text' },
9601017
searchSkip: { type: 'string', description: 'Number of earlier search results to skip' },
1018+
searchFilter: { type: 'string', description: 'Global OData filter for Dataverse Search' },
1019+
searchFacets: { type: 'string', description: 'JSON array of Dataverse Search facets' },
1020+
searchOrderBy: {
1021+
type: 'string',
1022+
description: 'JSON array of Dataverse Search sort expressions',
1023+
},
9611024
searchMode: { type: 'string', description: 'Search mode: any or all' },
9621025
searchType: { type: 'string', description: 'Search query type: simple or lucene' },
9631026
ownerType: { type: 'string', description: 'Owner type: user or team' },

apps/sim/hooks/queries/oauth/microsoft-dataverse-connections.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ describe('Microsoft Dataverse OAuth connections', () => {
7272
it('builds the exact environment-bound Better Auth link request', () => {
7373
const request = buildMicrosoftDataverseOAuthLinkRequest({
7474
callbackURL: 'https://sim.test/workflow?existing=1',
75+
draftId: 'draft-1',
7576
environmentUrl: ' https://contoso.crm4.dynamics.com/ ',
7677
})
7778

@@ -83,7 +84,10 @@ describe('Microsoft Dataverse OAuth connections', () => {
8384
'https://contoso.api.crm4.dynamics.com/.default',
8485
'offline_access',
8586
])
86-
expect(new URL(request.callbackURL).searchParams.get('__sim_dataverse_environment')).toBe(
87+
const callback = new URL(request.callbackURL)
88+
expect(callback.searchParams.get('existing')).toBe('1')
89+
expect(callback.searchParams.get('credentialDraftId')).toBe('draft-1')
90+
expect(callback.searchParams.get('__sim_dataverse_environment')).toBe(
8791
'https://contoso.api.crm4.dynamics.com'
8892
)
8993
})
@@ -95,14 +99,15 @@ describe('Microsoft Dataverse OAuth connections', () => {
9599
await act(async () => {
96100
await hook.result().mutateAsync({
97101
callbackURL: 'https://sim.test/workflow',
102+
draftId: 'draft-1',
98103
environmentUrl: 'https://contoso.crm.dynamics.com',
99104
})
100105
})
101106

102107
expect(mockLink).toHaveBeenCalledWith({
103108
providerId: 'microsoft-dataverse',
104109
callbackURL:
105-
'https://sim.test/workflow?__sim_dataverse_environment=https%3A%2F%2Fcontoso.api.crm.dynamics.com',
110+
'https://sim.test/workflow?credentialDraftId=draft-1&__sim_dataverse_environment=https%3A%2F%2Fcontoso.api.crm.dynamics.com',
106111
scopes: [
107112
'openid',
108113
'profile',

apps/sim/lib/oauth/microsoft-dataverse.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,7 @@ export function extractMicrosoftDataverseEnvironmentUrl(
258258
const candidate = value.slice(DATAVERSE_INSTANCE_MARKER_PREFIX.length)
259259
try {
260260
origins.add(normalizeMicrosoftDataverseEnvironmentUrl(candidate))
261-
} catch {
262-
// Ignore unrelated or hostile scopes rather than treating them as a trusted token audience.
263-
}
261+
} catch {}
264262
}
265263

266264
if (origins.size > 1) {

apps/sim/tools/generated/tool-ids.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/generated/tool-outputs.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/microsoft_dynamics_365/create_record.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const microsoftDynamics365CreateRecordTool: ToolConfig<
2222
id: 'microsoft_dynamics_365_create_record',
2323
name: 'Create Microsoft Dynamics 365 CRM Record',
2424
description:
25-
'Create a new record in a Microsoft Dataverse table. Requires the entity set name (plural table name) and record data as a JSON object.',
25+
'Create a new supported standard Microsoft Dynamics 365 CRM record using Dataverse logical column names.',
2626
version: '1.0.0',
2727

2828
oauth: { required: true, provider: 'microsoft-dataverse' },

0 commit comments

Comments
 (0)