Connect AI agents to Dynamics 365 (Sales, Service, Finance, Supply Chain) using Dataverse Web API (OData v4), F&O OData, and Azure OpenAI integration.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add frank-luongt/faos-skills-marketplace --skill dynamics-365 --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Dynamics 365?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/frank-luongt-dynamics-365-35bfba70)More formats (shields.io, HTML) on the badges page.
<!-- AUTO-GENERATED by export-skills.py — DO NOT EDIT -->
---
name: dynamics-365
description: Microsoft Dynamics 365 integration patterns for Sales, Service, Finance, and Supply Chain. Use when building AI agents that interact with Dataverse Web API (OData), F&O data entities, or integrate Azure OpenAI with Dynamics data.
tags: [dynamics-365, dataverse, microsoft, crm, erp]
---
# Microsoft Dynamics 365 Integration
Connect AI agents to Dynamics 365 (Sales, Service, Finance, Supply Chain) using Dataverse Web API (OData v4), F&O OData, and Azure OpenAI integration.
## When to Use
- Building AI agents that query or manage CRM data (leads, opportunities, cases, accounts)
- Integrating with Dynamics 365 Finance & Operations (journals, invoices, purchase orders)
- Combining Azure OpenAI with Dynamics data for deal analysis, case classification, or forecasting
- Using FetchXML for complex queries beyond OData $filter capability
- Triggering Power Automate flows from external AI agents
## API Surface
| Product | API Base URL | Style |
|---|---|---|
| **Sales / Service / Marketing** | `https://{org}.crm.dynamics.com/api/data/v9.2/` | Dataverse OData v4 |
| **Finance & Operations** | `https://{org}.cloudax.dynamics.com/data/` | F&O OData v4 |
| **Business Central** | `https://api.businesscentral.dynamics.com/v2.0/{tenant}/{env}/api/v2.0/` | BC OData v4 |
## Key Entities
### Sales
| Entity | OData Set | Key Fields |
|---|---|---|
| Account | `accounts` | `name`, `revenue`, `industrycode` |
| Contact | `contacts` | `fullname`, `emailaddress1`, `telephone1` |
| Lead | `leads` | `fullname`, `companyname`, `estimatedvalue`, `statuscode` |
| Opportunity | `opportunities` | `name`, `estimatedvalue`, `closeprobability`, `estimatedclosedate` |
| Quote | `quotes` | `name`, `totalamount` |
| Invoice | `invoices` | `name`, `totalamount`, `statuscode` |
### Customer Service
| Entity | OData Set | Key Fields |
|---|---|---|
| Case | `incidents` | `title`, `ticketnumber`, `prioritycode`, `statuscode` |
| Knowledge Article | `knowledgearticles` | `title`, `content`, `statecode` |
| Queue | `queues` | `name` |
### Finance & Operations
| Entity | OData Set | Key Fields |
|---|---|---|
| Journal Headers | `GeneralJournalHeaders` | Journal batch headers |
| Journal Lines | `GeneralJournalAccountEntries` | Line entries |
| Vendor Invoices | `VendorInvoiceHeaders` | AP invoices |
| Purchase Orders | `PurchaseOrderHeaders` | Procurement |
| Customers | `CustomersV3` | AR customers |
| Vendors | `VendorsV2` | AP vendors |
| Chart of Accounts | `MainAccounts` | GL accounts |
## Patterns
### 1. Authentication (Entra ID / OAuth 2.0)
```python
import msal
import requests
class Dynamics365Client:
"""Dynamics 365 client using MSAL (Entra ID) client credentials."""
def __init__(self, tenant_id: str, client_id: str, client_secret: str, crm_url: str):
self.crm_url = crm_url.rstrip("/")
self.api_url = f"{self.crm_url}/api/data/v9.2"
self._app = msal.ConfidentialClientApplication(
client_id,
authority=f"https://login.microsoftonline.com/{tenant_id}",
client_credential=client_secret,
)
self._scope = [f"{self.crm_url}/.default"]
self._session = requests.Session()
self._session.headers.update({
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json",
"Content-Type": "application/json; charset=utf-8",
"Prefer": "odata.include-annotations=*",
})
def _ensure_token(self):
token = self._app.acquire_token_for_client(scopes=self._scope)
if "access_token" not in token:
raise Exception(f"Auth failed: {token.get('error_description')}")
self._session.headers["Authorization"] = f"Bearer {token['access_token']}"
def get(self, entity_set: str, params: dict = None) -> dict:
self._ensure_token()
resp = self._session.get(f"{self.api_url}/{entity_set}", params=params)
resp.raise_for_status()
return resp.json()
def get_by_id(self, entity_set: str, record_id: str, select: str = None) -> dict:
self._ensure_token()
params = {"$select": select} if select else None
resp = self._session.get(f"{self.api_url}/{entity_set}({record_id})", params=params)
resp.raise_for_status()
return resp.json()
def create(self, entity_set: str, data: dict) -> str:
self._ensure_token()
resp = self._session.post(f"{self.api_url}/{entity_set}", json=data)
resp.raise_for_status()
return resp.headers.get("OData-EntityId", "")
def update(self, entity_set: str, record_id: str, data: dict) -> None:
self._ensure_token()
resp = self._session.patch(f"{self.api_url}/{entity_set}({record_id})", json=data)
resp.raise_for_status()
def delete(self, entity_set: str, record_id: str) -> None:
self._ensure_token()
resp = self._session.delete(f"{self.api_url}/{entity_set}({record_id})")
resp.raise_for_status()
```
### 2. Sales -- Leads, Opportunities, Accounts
```python
# Query high-value opportunities with account expansion
def get_top_opportunities(client: Dynamics365Client, min_value: float = 10000) -> list[dict]:
return client.get("opportunities", params={
"$select": "name,estimatedvalue,closeprobability,estimatedclosedate",
"$filter": f"statecode eq 0 and estimatedvalue gt {min_value}",
"$orderby": "estimatedvalue desc",
"$top": 20,
"$expand": "parentaccountid($select=name,revenue)",
}).get("value", [])
# Create a new lead
def create_lead(client: Dynamics365Client, first_name: str, last_name: str,
company: str, email: str, estimated_value: float = 0) -> str:
return client.create("leads", {
"firstname": first_name,
"lastname": last_name,
"companyname": company,
"emailaddress1": email,
"estimatedvalue": estimated_value,
"subject": f"Interest from {company}",
})
# Qualify a lead (bound action)
def qualify_lead(client: Dynamics365Client, lead_id: str) -> dict:
client._ensure_token()
resp = client._session.post(
f"{client.api_url}/leads({lead_id})/Microsoft.Dynamics.CRM.QualifyLead",
json={"CreateAccount": True, "CreateContact": True, "CreateOpportunity": True, "Status": 3},
)
resp.raise_for_status()
return resp.json()
# Update opportunity stage
def update_opportunity_stage(client: Dynamics365Client, opp_id: str,
stage: str, probability: int) -> None:
client.update("opportunities", opp_id, {
"stepname": stage,
"closeprobability": probability,
})
```
### 3. Customer Service -- Cases and Knowledge
```python
# Get open cases sorted by priority
def get_open_cases(client: Dynamics365Client, limit: int = 25) -> list[dict]:
return client.get("incidents", params={
"$select": "title,ticketnumber,prioritycode,statuscode,createdon",
"$filter": "statecode eq 0",
"$orderby": "prioritycode asc,createdon desc",
"$top": limit,
"$expand": "customerid_contact($select=fullname,emailaddress1)",
}).get("value", [])
# Create a support case
def create_case(client: Dynamics365Client, title: str, description: str,
customer_id: str, priority: int = 2) -> str:
return client.create("incidents", {
"title": title,
"description": description,
"prioritycode": priority,
"customerid_contact@odata.bind": f"/contacts({customer_id})",
})
# Search knowledge articles
def search_knowledge(client: Dynamics365Client, query: str) -> list[dict]:
return client.get("knowledgearticles", params={
"$select": "title,description,articlepublicnumber",
"$filter": f"statecode eq 3 and contains(title,'{query}')",
"$top": 10,
}).get("value", [])
```
### 4. Finance & Operations
```python
class D365FinOpsClient:
"""Dynamics 365 Finance & Operations OData client."""
def __init__(self, tenant_id: str, client_id: str, client_secret: str, fo_url: str):
self.fo_url = fo_url.rstrip("/")
self.data_url = f"{self.fo_url}/data"
self._app = msal.ConfidentialClientApplication(
client_id,
authority=f"https://login.microsoftonline.com/{tenant_id}",
client_credential=client_secret,
)
self._scope = [f"{self.fo_url}/.default"]
self._session = requests.Session()
self._session.headers.update({
"Content-Type": "application/json",
"Accept": "application/json",
})
def _ensure_token(self):
token = self._app.acquire_token_for_client(scopes=self._scope)
self._session.headers["Authorization"] = f"Bearer {token['access_token']}"
def get(self, entity: str, params: dict = None) -> list[dict]:
self._ensure_token()
resp = self._session.get(f"{self.data_url}/{entity}", params=params)
resp.raise_for_status()
return resp.json().get("value", [])
# Query vendor invoices
def get_vendor_invoices(client: D365FinOpsClient, from_date: str) -> list[dict]:
return client.get("VendorInvoiceHeaders", params={
"$filter": f"InvoiceDate gt {from_date}",
"$select": "InvoiceId,VendorName,InvoiceAmount,InvoiceDate",
"$top": 50,
"$orderby": "InvoiceDate desc",
})
# Query journal entries
def get_journal_entries(client: D365FinOpsClient, from_date: str) -> list[dict]:
return client.get("GeneralJournalAccountEntries", params={
"$filter": f"PostingDate gt {from_date}",
"$select": "JournalNumber,MainAccount,TransactionCurrencyAmount",
"$top": 100,
})
# Create a purchase order (F&O requires dataAreaId)
def create_purchase_order(client: D365FinOpsClient, po_number: str,
vendor: str, legal_entity: str = "usmf") -> dict:
client._ensure_token()
resp = client._session.post(f"{client.data_url}/PurchaseOrderHeaders", json={
"PurchaseOrderNumber": po_number,
"OrderVendorAccountNumber": vendor,
"dataAreaId": legal_entity,
})
resp.raise_for_status()
return resp.json()
```
### 5. FetchXML (Complex Queries)
```python
import urllib.parse
def fetch_xml_query(client: Dynamics365Client, entity_set: str, fetch_xml: str) -> list[dict]:
"""Execute a FetchXML query for complex joins and aggregations."""
client._ensure_token()
encoded = urllib.parse.quote(fetch_xml)
resp = client._session.get(f"{client.api_url}/{entity_set}?fetchXml={encoded}")
resp.raise_for_status()
return resp.json().get("value", [])
# High-value opportunities in a specific industry
opps = fetch_xml_query(client, "opportunities", """
<fetch top="50">
<entity name="opportunity">
<attribute name="name"/>
<attribute name="estimatedvalue"/>
<attribute name="closeprobability"/>
<filter>
<condition attribute="statecode" operator="eq" value="0"/>
<condition attribute="estimatedvalue" operator="gt" value="50000"/>
</filter>
<link-entity name="account" from="accountid" to="parentaccountid" alias="acct">
<attribute name="name"/>
<filter>
<condition attribute="industrycode" operator="eq" value="6"/>
</filter>
</link-entity>
<order attribute="estimatedvalue" descending="true"/>
</entity>
</fetch>
""")
```
### 6. Change Tracking (Delta Sync)
```python
def get_changes(client: Dynamics365Client, entity_set: str,
delta_link: str = None) -> tuple[list[dict], str]:
"""Track record changes since last sync.
Returns (changed_records, next_delta_link).
"""
client._ensure_token()
if delta_link:
resp = client._session.get(delta_link)
else:
resp = client._session.get(
f"{client.api_url}/{entity_set}",
headers={"Prefer": "odata.track-changes"},
)
resp.raise_for_status()
data = resp.json()
return data.get("value", []), data.get("@odata.deltaLink", "")
# Initial sync
records, delta = get_changes(client, "accounts")
# Subsequent sync -- only get changes
changed, delta = get_changes(client, "accounts", delta_link=delta)
```
### 7. Pagination Helper
```python
def get_all_records(client: Dynamics365Client, entity_set: str,
params: dict = None) -> list[dict]:
"""Handle server-side paging (5000 record default page size)."""
client._ensure_token()
records = []
url = f"{client.api_url}/{entity_set}"
while url:
resp = client._session.get(url, params=params)
resp.raise_for_status()
data = resp.json()
records.extend(data.get("value", []))
url = data.get("@odata.nextLink")
params = None # nextLink already contains params
return records
```
### 8. Azure OpenAI + Dynamics 365
```python
import openai
def classify_case_with_ai(client: Dynamics365Client, case_id: str,
aoai_client: openai.AzureOpenAI) -> str:
"""Classify a D365 case using Azure OpenAI and update the record."""
case = client.get_by_id("incidents", case_id, select="title,description")
classification = aoai_client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Classify the support case into: Billing, Technical, Account, General. Return only the category."},
{"role": "user", "content": f"Title: {case['title']}\nDescription: {case.get('description', 'N/A')}"},
],
)
category = classification.choices[0].message.content.strip()
client.update("incidents", case_id, {"new_aicategory": category})
return category
def analyze_deal_risk(client: Dynamics365Client, opp_id: str,
aoai_client: openai.AzureOpenAI) -> str:
"""Analyze deal risk using Azure OpenAI."""
opp = client.get_by_id("opportunities", opp_id,
select="name,estimatedvalue,closeprobability,estimatedclosedate,modifiedon")
response = aoai_client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a sales operations analyst. Assess deal risk."},
{"role": "user", "content": f"Opportunity: {opp['name']}, Value: ${opp.get('estimatedvalue', 0):,.2f}, "
f"Probability: {opp.get('closeprobability', 'N/A')}%, "
f"Close date: {opp.get('estimatedclosedate', 'N/A')}, "
f"Last modified: {opp.get('modifiedon', 'N/A')}"},
],
)
return response.choices[0].message.content
```
### 9. Agent Tool Functions
```python
def search_accounts(account_name: str) -> str:
"""Search Dynamics 365 accounts by name.
Args:
account_name: Full or partial account name
"""
client = Dynamics365Client(TENANT_ID, CLIENT_ID, CLIENT_SECRET, CRM_URL)
accounts = client.get("accounts", params={
"$select": "name,revenue,telephone1",
"$filter": f"contains(name,'{account_name}')",
"$top": 10,
}).get("value", [])
if not accounts:
return f"No accounts found matching '{account_name}'."
lines = [f"- {a['name']} (Revenue: ${a.get('revenue', 0):,.0f})" for a in accounts]
return f"Found {len(accounts)} account(s):\n" + "\n".join(lines)
def get_pipeline_summary() -> str:
"""Get a summary of the open sales pipeline."""
client = Dynamics365Client(TENANT_ID, CLIENT_ID, CLIENT_SECRET, CRM_URL)
opps = client.get("opportunities", params={
"$select": "name,estimatedvalue,closeprobability",
"$filter": "statecode eq 0",
"$orderby": "estimatedvalue desc",
}).get("value", [])
total = sum(o.get("estimatedvalue", 0) for o in opps)
weighted = sum(o.get("estimatedvalue", 0) * o.get("closeprobability", 0) / 100 for o in opps)
return (f"Pipeline: {len(opps)} open opportunities\n"
f"Total value: ${total:,.0f}\n"
f"Weighted value: ${weighted:,.0f}")
```
## Dynamics 365 Copilot Features
| Module | Copilot Capabilities |
|---|---|
| **Sales** | Record summarization, email drafting, meeting prep, lead research, pipeline insights |
| **Service** | Case summarization, KB article generation, similar case suggestions, AI routing |
| **Finance** | Account reconciliation agent, collections drafting, cash flow forecasting |
| **Supply Chain** | Demand forecasting, intelligent order management, inventory anomaly detection |
## Anti-Patterns
- Not using `$select` -- always specify fields to reduce payload and improve performance
- Ignoring `@odata.nextLink` -- results are capped at 5000 per page by default
- Using OData `$filter` for complex joins -- use FetchXML instead
- Hardcoding GUIDs -- use alternate keys where available (e.g., `accounts(accountnumber='A001')`)
- Skipping Application User setup -- client credentials flow requires a D365 Application User with security roles
- Sending raw D365 data to LLMs without field selection -- filter sensitive fields before AI processing
- Not handling F&O `dataAreaId` -- Finance & Operations requires legal entity on most write operations
## References
- [Dataverse Web API Reference](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/about)
- [Dynamics 365 F&O Data Entities](https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/data-entities)
- [MSAL Python Documentation](https://learn.microsoft.com/en-us/entra/msal/python/)
- [FetchXML Reference](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/use-fetchxml-construct-query)
- [Dynamics 365 Copilot](https://learn.microsoft.com/en-us/dynamics365/copilot/ai-get-started)
<!-- Source: .faos/custom/skills/integrations/dynamics-365/SKILL.md -->
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!