# Openclaw Dynamics 365

> Create and update CRM records in Microsoft Dynamics 365 — Opportunities, Leads, Contacts, Accounts, and Tasks via Dataverse Web API with Azure AD OAuth 2.0.

- Skill: `lord1egypt/openclaw-dynamics-365` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lord1egypt/openclaw-dynamics-365`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lord1egypt/openclaw-dynamics-365/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Lord1Egypt (https://skillmd.com/u/lord1egypt)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/lord1egypt/openclaw-dynamics-365

---


# Dynamics 365 CRM Skill

## When to activate

Activate this skill when the user mentions **Microsoft Dynamics 365**, **D365**, **Dynamics CRM**, or any of the following actions directed at a Dynamics 365 CRM:

- Creating or updating an **opportunity** / deal / oportunidade
- Creating or updating a **lead**
- Creating or updating a **contact** / contacto
- Creating or updating an **account** / empresa / conta
- Creating a **task** / activity linked to a CRM record

## Credentials required

| Variable | Description |
|---|---|
| `DYNAMICS365_CLIENT_ID` | Azure AD Application (client) ID |
| `DYNAMICS365_CLIENT_SECRET` | Azure AD client secret |
| `DYNAMICS365_TENANT_ID` | Azure AD Tenant ID — use `common` for multi-tenant |
| `DYNAMICS365_INSTANCE_URL` | Dynamics 365 instance URL, e.g. `https://contoso.crm.dynamics.com` |

## Step-by-step execution

### 1 — Identify intent and entity type

Determine from the user's message which entity to create or update:

| User says | Entity | Action |
|---|---|---|
| "create deal", "new opportunity", "deal com X" | Opportunity | upsert |
| "new lead", "criar lead", "prospect X" | Lead | upsert |
| "add contact", "criar contacto", "cliente X no CRM" | Contact | upsert |
| "new account", "empresa Y no CRM", "criar conta" | Account | upsert |
| "create task", "follow-up", "lembrete para X" | Task | create |

### 2 — Extract fields from user input

**Opportunity fields:**
- `name` — deal / opportunity name (required)
- `estimatedvalue` — numeric value in local currency
- `stepname` — pipeline stage: "Qualify" / "Develop" / "Propose" / "Close"
- `actualclosedate` — expected close date (ISO: YYYY-MM-DD)

**Lead fields:**
- `fullname` — full name (required)
- `emailaddress1` — email address
- `companyname` — company name
- `telephone1` — phone number
- `jobtitle` — job title

**Contact fields:**
- `fullname` — full name (required)
- `emailaddress1` — email address
- `telephone1` — phone number
- `jobtitle` — job title

**Account fields:**
- `name` — company / account name (required)
- `emailaddress1` — email address
- `telephone1` — phone number
- `websiteurl` — website

**Task fields:**
- `subject` — task title (required)
- `description` — details / notes
- `scheduledend` — due date-time (ISO 8601)
- `prioritycode` — 0 = Low, 1 = Normal, 2 = High

### 3 — Call the appropriate method

Use the `Dynamics365Client` from the `openclaw-dynamics-365` npm package:

```typescript
import { Dynamics365Client } from "openclaw-dynamics-365";

const client = new Dynamics365Client(
  process.env.DYNAMICS365_INSTANCE_URL,
  accessToken,  // obtained via OAuth flow
);

// Opportunity
const result = await client.upsertOpportunity({ name: "Deal Contoso", estimatedvalue: 50000 });

// Lead
const result = await client.upsertLead({ fullname: "João Silva", companyname: "TechNova" });

// Contact
const result = await client.upsertContact({ fullname: "Maria Santos", emailaddress1: "maria@techno.com" });

// Account
const result = await client.upsertAccount({ name: "TechNova Ltda" });

// Task
const taskId = await client.createTask({ subject: "Follow-up call", prioritycode: 2 });
```

### 4 — Format response to the user

On success:
```
✅ [Entity] "[name]" [created/updated] in Dynamics 365.
🔗 [record URL]
```

On error:
```
❌ Could not [create/update] [entity] in Dynamics 365: [error message]
```

## OAuth setup

For initial authorization, use `getAuthorizationUrl()` to redirect the user to
Microsoft's consent screen, then `exchangeCodeForTokens()` in the callback:

```typescript
import { getAuthorizationUrl, exchangeCodeForTokens } from "openclaw-dynamics-365";

// 1. Redirect user
const url = getAuthorizationUrl(config, redirectUri, state);

// 2. In callback
const tokens = await exchangeCodeForTokens(config, code, redirectUri);

// 3. Refresh when expired (~1h)
if (isTokenExpired(tokens)) {
  tokens = await refreshAccessToken(config, tokens.refreshToken);
}
```

## Notes

- The Dataverse Web API uses **OData v4** — all queries use `$filter`, `$select`, `$top`
- Upsert operations search by name/email first to avoid duplicates
- Lookup fields (e.g. linking a Contact to an Account) use OData binding syntax:
  `"parentcustomerid_account@odata.bind": "/accounts({accountId})"`
- Rate limit: **6,000 requests / 5 minutes** per user — well within typical usage
- Access tokens expire after **~1 hour** — implement refresh logic using `refreshAccessToken()`

