# How to Integrate Retool with Microsoft Dynamics 365

- Tool: Retool
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: April 2026

## TL;DR

Connect Retool to Microsoft Dynamics 365 by creating a REST API Resource pointed at your Dynamics environment's Web API endpoint, authenticated with Azure AD OAuth 2.0. Use OData v4 query syntax ($filter, $select, $expand) to retrieve contacts, leads, opportunities, and cases, then build sales operations dashboards that combine Dynamics CRM data with other business systems.

## Why Connect Retool to Microsoft Dynamics 365?

Dynamics 365 is one of the most widely deployed enterprise CRM platforms, particularly in organizations already invested in the Microsoft ecosystem. While Dynamics has robust built-in sales and customer service interfaces, operations teams often need CRM data surfaced alongside data from other systems — internal databases, support tools, financial systems — in a unified internal tool. Retool makes this possible by treating Dynamics 365 as one of several data sources in a multi-resource app.

Common scenarios include: a sales operations dashboard that combines Dynamics opportunity data with revenue metrics from an internal database; a customer success panel that shows Dynamics contact history alongside support ticket counts and product usage data; or a custom case management interface for teams that find Dynamics' built-in case view too complex for their specific workflow. Retool's visual builder lets you create exactly the interface your team needs, not whatever Dynamics' default layout provides.

The Dynamics 365 Web API is built on Dataverse and uses OData v4 (Open Data Protocol), a standard query language based on REST that uses URL parameters for filtering, selecting fields, and expanding related entities. This is different from SQL databases or even REST APIs with simple JSON bodies — OData syntax like $filter=statuscode eq 1 and $expand=customerid_contact takes some getting used to, but it is well-documented and follows consistent patterns across all Dynamics entities.

## Before you start

- A Retool account (Cloud or self-hosted) with permission to add Resources
- An Azure Active Directory tenant with admin access to register an app registration
- A Dynamics 365 environment with your organization's CRM data, and your environment's URL (format: https://yourorg.crm.dynamics.com)
- Dynamics 365 security role that grants API access — by default, users need the 'Dynamics 365 Administrator' or a custom role with Web API access enabled
- Basic familiarity with OData v4 query syntax ($filter, $select, $expand) or willingness to learn the patterns covered in this guide

## Step-by-step guide

### 1. Register an Azure AD app for Dynamics 365 Web API access

Dynamics 365 uses Azure Active Directory for all API authentication. You must register an app in your Azure AD tenant that has permission to access the Dynamics 365 API.

Go to portal.azure.com and navigate to Azure Active Directory → App Registrations → New Registration. Fill in the form:
- Name: 'Retool Dynamics 365 Integration'
- Supported account types: 'Accounts in this organizational directory only'
- Redirect URI: Select 'Web' and enter your Retool OAuth callback URL (https://oauth.retool.com/oauth/user/oauthcallback for Retool Cloud; for self-hosted, use https://your-retool.company.com/oauth/user/oauthcallback)

Click Register. Note the Application (client) ID and Directory (tenant) ID from the overview page.

Navigate to Certificates & Secrets → Client Secrets → New Client Secret. Add a description, set an expiry, and copy the Value immediately.

Navigate to API Permissions → Add a Permission → Dynamics CRM (may also appear as 'Common Data Service' in some tenants). Select Delegated Permissions and add: user_impersonation. This is the primary permission needed for the Dynamics 365 Web API. Click Grant Admin Consent to pre-approve.

Critical: also navigate to Authentication for the app registration and verify the Redirect URIs are saved correctly. For Retool Cloud, add both https://oauth.retool.com/oauth/user/oauthcallback and https://oauth.retool.com/oauth/app/oauthcallback to handle both user-level and app-level OAuth flows.

**Expected result:** An Azure AD app registration exists with client ID, client secret, tenant ID, and Dynamics CRM user_impersonation permission granted with admin consent. The redirect URI matches your Retool instance's OAuth callback URL.

### 2. Create the Dynamics 365 REST API Resource in Retool

Open Retool and navigate to the Resources tab. Click Add Resource and select REST API. Configure:
- Name: 'Microsoft Dynamics 365'
- Base URL: https://yourorg.crm.dynamics.com/api/data/v9.2 (replace 'yourorg' with your Dynamics environment identifier, and confirm the API version — v9.2 is standard for current Dynamics 365)

For authentication, select OAuth 2.0:
- Grant type: Authorization Code
- Authorization URL: https://login.microsoftonline.com/{{ YOUR_TENANT_ID }}/oauth2/v2.0/authorize
- Access Token URL: https://login.microsoftonline.com/{{ YOUR_TENANT_ID }}/oauth2/v2.0/token
- Client ID: your Azure AD app's Application (client) ID
- Client Secret: the client secret value
- Scope: https://yourorg.crm.dynamics.com/.default offline_access (the resource URL must match your Dynamics environment URL exactly, including the trailing slash behavior — test both with and without if you get scope errors)

Add global headers that apply to all Dynamics 365 API requests:
- OData-MaxVersion: 4.0
- OData-Version: 4.0
- Accept: application/json
- Content-Type: application/json

These headers are required by the Dynamics 365 Web API and must be present on every request. Click Save Changes, then click Connect Account and authenticate with a Dynamics 365 account that has the appropriate access level.

**Expected result:** The Dynamics 365 REST API Resource appears in the Resources list with a connected OAuth status. A test GET request to /contacts?$top=1 returns a JSON response with at least one contact record, confirming authentication and base URL are correctly configured.

### 3. Query Dynamics 365 entities with OData v4 syntax

Dynamics 365's Web API uses OData v4, which means all query filtering, field selection, and relationship expansion happens through URL query parameters rather than a query body. Understanding the core OData operators is essential for building useful Retool queries.

Create a new query in your Retool app using the Dynamics 365 resource:
- Method: GET
- Path: /contacts
- URL Parameters (add these as key-value pairs in Retool's URL Params section):
  - $select: firstname,lastname,emailaddress1,telephone1,createdon,statecode
  - $filter: statecode eq 0 (active contacts only; statecode 1 = inactive)
  - $top: 50
  - $orderby: createdon desc

OData $filter syntax uses entity property names (not display names) with comparison operators: eq (equals), ne (not equals), gt (greater than), lt (less than), contains(field,'value') for text search, and (AND), or (OR). Property names are case-sensitive and use the schema names from Dynamics' metadata, not the display labels shown in the Dynamics UI.

To search contacts by name, add a filter that changes when a search input component updates:
- $filter: contains(fullname,'{{ contactSearch.value }}')

Dynamics returns responses wrapped in a 'value' array with OData metadata: { '@odata.context': '...', 'value': [...] }. Add a transformer to extract the value array and shape the data for display.

For relationships between entities (e.g., getting account name with a contact), use $expand: $expand=parentcustomerid_account($select=name,telephone1) to include related account data in the same request.

```
// JavaScript transformer for Dynamics 365 contacts response
// OData responses wrap results in a 'value' array with metadata overhead
const contacts = data.value || [];

return contacts.map(c => ({
  id: c.contactid,
  full_name: c.fullname || [c.firstname, c.lastname].filter(Boolean).join(' '),
  email: c.emailaddress1 || '',
  phone: c.telephone1 || '',
  company: c.parentcustomerid_account?.name || c.parentcustomeridname || '',
  job_title: c.jobtitle || '',
  created: c.createdon
    ? new Date(c.createdon).toLocaleDateString()
    : '',
  status: c.statecode === 0 ? 'Active' : 'Inactive',
  // OData navigation properties use GUID suffix
  account_id: c['_parentcustomerid_value'] || ''
}));
```

**Expected result:** A contacts table populates with Dynamics 365 contact records. Typing in a search input filters the results in real time via the OData $filter parameter. The table shows clean, formatted data with account names correctly resolved via the $expand parameter.

### 4. Build a sales opportunities pipeline view

Opportunities are the core entity for sales teams in Dynamics 365. Build a pipeline view that shows all open opportunities grouped by sales stage.

Create a query targeting the opportunities entity:
- Method: GET
- Path: /opportunities
- URL Parameters:
  - $select: name,estimatedvalue,actualclosedate,stepname,statecode,createdon,_ownerid_value
  - $filter: statecode eq 0 (open opportunities only; statecode 1 = won, 2 = lost)
  - $expand: ownerid($select=fullname) (expand to get the owner's name)
  - $orderby: estimatedvalue desc
  - $top: 100

Add transformer to format the Dynamics response including proper currency formatting and date display. The estimatedvalue field returns a number; format it as currency. Dates come as ISO strings from Dynamics.

Drag a Table component bound to the opportunities data. Add a Chart component (Bar type) bound to a secondary transformer that groups opportunities by stepname (pipeline stage) and sums estimatedvalue for each stage — this creates the pipeline summary chart.

For the pipeline by stage chart, you need an additional JavaScript transformer:

Add filters above the table: a Select for statecode (Open, Won, Lost), a date range picker for createdon, and an owner filter populated from a separate /systemusers query. Wire all filters to the opportunity query's URL parameters. Add 'Only run when' conditions where needed to prevent empty-state errors.

To see the full combined architecture of Dynamics data with data from other Retool resources, RapidDev's team can help design multi-source apps that join Dynamics opportunities with your internal revenue database and product usage metrics into unified sales dashboards.

```
// JavaScript transformer for opportunities pipeline chart
// Groups opportunity value by sales stage
const opportunities = opportunitiesQuery.data || []; // use transformer output

const pipelineByStage = opportunities.reduce((acc, opp) => {
  const stage = opp.stepname || 'No Stage';
  if (!acc[stage]) {
    acc[stage] = { stage, total_value: 0, count: 0 };
  }
  acc[stage].total_value += parseFloat(opp.estimatedvalue) || 0;
  acc[stage].count += 1;
  return acc;
}, {});

// Define standard Dynamics sales stages for consistent ordering
const stageOrder = [
  'Qualify', 'Develop', 'Propose', 'Close', 'Won'
];

return Object.values(pipelineByStage)
  .sort((a, b) => {
    const aIdx = stageOrder.indexOf(a.stage);
    const bIdx = stageOrder.indexOf(b.stage);
    return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
  })
  .map(s => ({
    stage: s.stage,
    deal_count: s.count,
    total_value: '$' + s.total_value.toLocaleString(undefined, { maximumFractionDigits: 0 }),
    raw_value: s.total_value
  }));
```

**Expected result:** A table shows all open Dynamics opportunities with owner names, estimated values, and pipeline stages. A bar chart shows total pipeline value by stage. Filters for owner and date range update the data dynamically. The pipeline value chart refreshes when filter selections change.

### 5. Create, update, and manage Dynamics 365 records from Retool

Beyond reading data, Retool can create and update Dynamics 365 records through the Web API. Write operations follow the same OData pattern but use POST (create), PATCH (update), and DELETE methods.

To update a Dynamics record (e.g., update a contact's phone number):
- Method: PATCH
- Path: /contacts({{ contactsTable.selectedRow.id }}) — note the GUID in parentheses is part of the URL path, not a query parameter
- Body type: JSON
- Body: { "telephone1": "{{ phoneInput.value }}" } — include only the fields being updated

Important: Dynamics PATCH requests use 'merge semantics' — only the fields you include in the body are updated. Omitted fields retain their current values. This is different from PUT operations which would overwrite the entire record.

To create a new contact:
- Method: POST
- Path: /contacts
- Body: a JSON object with the required and desired fields. The minimum required fields for a contact are usually just firstname or lastname.

To relate a contact to an account on creation, use the OData binding syntax:
- Include in the POST body: 'parentcustomerid_account@odata.bind': '/accounts(' + selectedAccountId + ')'

Set all write queries to Manual trigger. Add confirmation dialogs for updates and deletes. Add On Success event handlers that refresh the relevant read queries so the table updates immediately after a successful write.

For the delete pattern: use DELETE method with path /contacts({{ selectedRowId }}). Always add a strong confirmation dialog — deleted Dynamics records may be recoverable via the recycle bin depending on your org's settings, but treat deletes as permanent from a UX perspective.

```
// POST body for creating a new Dynamics 365 contact
// Enter this in the query body as JSON
{
  "firstname": "{{ firstNameInput.value }}",
  "lastname": "{{ lastNameInput.value }}",
  "emailaddress1": "{{ emailInput.value }}",
  "telephone1": "{{ phoneInput.value }}",
  "jobtitle": "{{ jobTitleInput.value }}",
  // Bind to an existing account using OData navigation property syntax
  "parentcustomerid_account@odata.bind": "/accounts({{ accountSelect.value }})"
}

// PATCH body for updating an existing opportunity's estimated value
// Path: /opportunities({{ selectedOpportunity.id }})
{
  "estimatedvalue": {{ parseFloat(newValueInput.value) }},
  "actualclosedate": "{{ closeDatePicker.value }}"
}
```

**Expected result:** The update form allows editing contact details. Clicking Save triggers the PATCH query, receives a 204 response, and the contacts table refreshes showing the updated values. Create operations add new records that appear in the table after refresh. All write operations are guarded by confirmation dialogs.

## Best practices

- Always use $select in OData queries to specify only the fields you need — Dynamics entities can have 100+ fields, and fetching all of them significantly slows query performance.
- Create a dedicated Azure AD service account for the Retool connection with 'Share credentials between users' enabled, scoped to read (and write where needed) on specific Dynamics security roles.
- Store Azure AD client secrets in Retool Configuration Variables marked as secret with rotation reminders — Azure client secrets expire and will break the integration if not renewed.
- Use OData $filter with statecode constraints on all entity queries (statecode eq 0 for active records) to avoid retrieving deactivated or archived records in operational dashboards.
- When referencing related entity navigation properties (lookups), use $expand sparingly — each expanded relationship adds API latency. For read-heavy dashboards, consider fetching related data separately and joining in a JavaScript transformer.
- Add 'Only run when' conditions on all write queries (PATCH, POST, DELETE) to prevent accidental execution when no record is selected.
- Test OData queries using a REST client (Postman, curl, or the Dynamics Web API browser interface at yourorg.crm.dynamics.com/api/data/v9.2/) before building them into Retool — this speeds up debugging authentication and syntax issues.
- For dashboards with multiple Dynamics queries, consider using Retool Workflows for data-heavy aggregation tasks that combine multiple Dynamics API calls, keeping the app-level queries fast and focused.

## Use cases

### Build a sales pipeline operations dashboard

Create a Retool dashboard that shows all open opportunities in Dynamics 365 with their stage, estimated close date, assigned owner, and deal value. Sales operations managers can filter by territory, owner, or stage, see a pipeline summary chart showing total deal value by stage, and identify opportunities that haven't been updated in more than 14 days. The dashboard gives leadership a real-time pipeline view without needing Dynamics licenses for all viewers.

Prompt example:

```
Build a sales pipeline dashboard that queries all open Dynamics 365 opportunities ($filter=statecode eq 0) with fields: name, estimatedvalue, stepname, estimatedclosedate, and the owning user's name via $expand. Show a kanban-style or tabular view grouped by pipeline stage, a bar chart of total deal value by stage, and a filter for opportunities not updated in 14+ days.
```

### Build a contact and account management admin panel

Create a Retool app for operations teams that lists Dynamics 365 accounts with their associated contacts, recent activities, and open cases. When an operator selects an account, the right panel shows all contacts at that account, their communication history from Dynamics activity records, and any open service cases. This unified view replaces navigating between three separate Dynamics sections and makes customer conversations faster.

Prompt example:

```
Create a Dynamics CRM admin panel with an account search input. When an account is selected, show: (1) the account details with industry, revenue, and assigned owner; (2) a list of all contacts at that account via $expand; (3) recent activities (emails, calls) from the activitypointers entity; (4) open service cases filtered by customerid matching the selected account.
```

### Build a lead qualification and routing tool

Create a Retool app where sales development reps review inbound Dynamics 365 leads, enrich them with internal data, and route qualified leads to the appropriate sales owner. The panel shows new leads from the last 7 days with their source, status, and rating. Reps can update the lead's qualification status, assign an owner from a dropdown, and convert qualified leads to contacts/opportunities — all without navigating Dynamics' full UI for each record.

Prompt example:

```
Build a lead management panel that shows Dynamics 365 leads from the last 7 days with status 'New' ($filter=statuscode eq 1 and createdon gt last-week). Include a status update dropdown (New, Contacted, Qualified, Disqualified), an owner assignment select populated from Dynamics systemusers, and a 'Convert to Opportunity' button that creates an opportunity record via a POST to the opportunities entity.
```

## Troubleshooting

### OAuth authentication returns 'AADSTS700016: Application with identifier was not found in the directory' during the Retool connect flow

Cause: The Azure AD application was registered in a different tenant than the one being used for authentication, the client ID is incorrect, or the app was deleted or disabled in Azure AD.

Solution: Verify the Application (client) ID in Retool exactly matches the Application ID shown in Azure AD → App Registrations → (your app) → Overview. Also verify that the tenant ID in the OAuth authorization and token URLs matches the tenant where the app is registered. If the organization has multiple Azure tenants, confirm you are looking at the correct one.

### Dynamics 365 queries return 401 Unauthorized with 'message: Access token is empty or null' after successful OAuth connection

Cause: The OAuth scope is not correctly including the Dynamics resource URL, so the access token obtained is valid for other Microsoft services (e.g., Microsoft Graph) but not for the specific Dynamics environment.

Solution: Verify that the OAuth scope in the Retool resource configuration matches your exact Dynamics environment URL: https://yourorg.crm.dynamics.com/.default (not a generic Microsoft scope). The resource URL in the scope must exactly match your environment's URL including any regional subdomain (e.g., .crm4.dynamics.com for European environments). Update the scope and re-authenticate.

### OData query returns 400 Bad Request with 'Could not find a property named X on type Y'

Cause: The property names in $select, $filter, or $orderby use display labels or incorrect schema names instead of the Dynamics entity's actual OData property names, which are case-sensitive schema names.

Solution: Look up the correct schema property names using the Dynamics metadata endpoint (GET https://yourorg.crm.dynamics.com/api/data/v9.2/EntityDefinitions?$select=LogicalName for entity names, then GET the specific entity definition for field names), or check the Power Apps maker portal under Solutions → (your solution) → (entity) → Fields. Common mistakes: 'Name' vs 'name', 'Email' vs 'emailaddress1', 'Phone' vs 'telephone1'.

```
// Correct OData property names for common Dynamics entities
// Contact: firstname, lastname, fullname, emailaddress1, telephone1, jobtitle
// Account: name, telephone1, emailaddress1, revenue, industrycode
// Opportunity: name, estimatedvalue, actualclosedate, stepname, statecode
// Case (Incident): title, description, statecode, prioritycode, ticketnumber
```

### PATCH request to update a Dynamics record returns 'Response contains no Content' and the table data does not update

Cause: Dynamics 365 PATCH requests return HTTP 204 No Content on success — this is the expected behavior, not an error. Retool may display this as an empty response, which appears to be a failure but is actually success.

Solution: Configure the query's On Success event handler to trigger a data refresh query (re-run the GET contacts or opportunities query) rather than checking the PATCH response body for confirmation. A 204 status code is Dynamics' success response for PATCH operations. If you need the updated record data immediately, add a second query that GETs the specific record by ID after the PATCH.

## Frequently asked questions

### What is OData and why does Dynamics 365 use it instead of standard REST API query syntax?

OData (Open Data Protocol) is a standardized REST-based protocol for querying and manipulating data resources. Microsoft chose OData for Dynamics 365 and the broader Dataverse platform because it provides a consistent query language across all entities without requiring different endpoint formats for different operations. OData $filter, $select, $expand, and $orderby parameters work identically for contacts, opportunities, accounts, and custom entities. While the syntax differs from SQL or typical REST API patterns, it is well-documented and follows predictable rules once learned.

### Do I need Dynamics 365 licenses for all Retool users who will use the integration?

This depends on how you configure the OAuth connection. If you use a single shared service account with 'Share credentials between users' enabled in Retool, only the service account needs a Dynamics 365 license — all Retool users share that account's access. If users authenticate individually (per-user OAuth), each user needs their own Dynamics 365 license. For read-only operational dashboards where all users need the same data, the shared service account approach is most practical and cost-effective.

### How do I find the correct OData property names for Dynamics 365 fields?

The best methods are: (1) the Dynamics metadata API at https://yourorg.crm.dynamics.com/api/data/v9.2/$metadata, which returns XML with all entity and property schema names; (2) the Power Apps maker portal at make.powerapps.com → your environment → Data → Tables → (entity) → Columns, which shows display names alongside schema names; (3) looking at a sample API response in Retool's query panel with $top=1 and no $select, which shows all properties for one record with their exact key names.

### Can Retool connect to Dynamics 365 on-premises (not Dynamics 365 Online)?

Retool Cloud can connect to Dynamics 365 Online (Microsoft-hosted) using the Azure AD OAuth approach described in this guide. Connecting to Dynamics CRM on-premises (hosted in your own data center) requires either self-hosted Retool within your network, or exposing the on-premises Dynamics Web API endpoint publicly (with appropriate security controls). The authentication mechanism for on-premises Dynamics uses Claims-based authentication or Windows Authentication, which requires additional configuration beyond the standard Azure AD OAuth flow.

### How do I filter Dynamics 365 data by a date range in Retool?

Use OData $filter with ISO 8601 datetime comparisons. Dynamics 365 date fields store UTC datetimes, so filter values should be in UTC or the API handles conversion. In Retool, reference a DateRangePicker component: $filter=createdon ge {{ new Date(dateRangePicker.startDate).toISOString() }} and createdon le {{ new Date(dateRangePicker.endDate).toISOString() }}. For 'today' filters without a picker: $filter=createdon ge {{ new Date(new Date().setHours(0,0,0,0)).toISOString() }}.

---

Source: https://www.rapidevelopers.com/retool-integrations/microsoft-dynamics-365
© RapidDev — https://www.rapidevelopers.com/retool-integrations/microsoft-dynamics-365
