📌 Overview

This guide walks you through building a scheduled sync that reads users from Microsoft Entra ID (Azure AD) via Microsoft Graph and upserts them into a Dataverse table. It covers:

Note: The older Azure AD/Entra connector “List users” action is no longer available. Use Microsoft Graph via HTTP.

🧱 Prerequisites

  1. Dataverse Environment with appropriate maker/admin permissions.

  2. Dataverse table to store AD users (e.g., AAD Users).

  3. Azure App Registration (client credentials flow).

  4. Admin consent for Microsoft Graph application permissions.

  5. Power Automate (Cloud) access with HTTP premium action.

🗃️ Dataverse Table Design

Table: AAD Users (logical name e.g., contoso_aadusers)

Recommended columns (logical names shown as examples):

✅ Create an Alternate Key on either contoso_azureadobjectid (preferred) or contoso_userprincipalname to maintain data integrity and prevent duplicates.

1

🔐 Azure App Registration (Microsoft Entra ID → App registrations)

  1. Register app

    • Name: AD-User-Sync-App

    • Supported account types: Single tenant.

  2. Client secret

    • Certificates & secrets → New client secret.

    • Copy Client ID, Tenant ID, and Secret (store securely—never paste in chat or logs).

  3. Permissions (Application)

    • Microsoft Graph → Application permissions

      • User.Read.All

      • Directory.Read.All

    • Click Grant admin consent and ensure status is Granted.

2

🛠️ Step-by-Step: Build the Scheduled Flow

1) Create a Scheduled Cloud Flow

2) Add HTTP action (Premium)

URI (single line):

https://graph.microsoft.com/v1.0/users?$select=id,displayName,mail,jobTitle,department,userPrincipalName,employeeId,accountEnabled,mobilePhone,companyName,userType

3

3) Add Parse JSON

Content:

@body('HTTP')

Schema (object with value array):


{
  "type": "object",
  "properties": {
    "value": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "displayName": { "type": ["string","null"] },
          "mail": { "type": ["string","null"] },
          "jobTitle": { "type": ["string","null"] },
          "department": { "type": ["string","null"] },
          "userPrincipalName": { "type": "string" },
          "employeeId": { "type": ["string","null"] },
          "accountEnabled": { "type": "boolean" },
          "mobilePhone": { "type": ["string","null"] },
          "companyName": { "type": ["string","null"] },
          "userType": { "type": ["string","null"] }
        }
      }
    }
  }
}

4) Add Apply to each

5

5) Dataverse → List rows (check if record exists)

6

Option A (preferred: AAD Object ID)

contoso_azureadobjectid eq '@{items('Apply_to_each')?['id']}'

Option B (UPN)

contoso_userprincipalname eq '@{replace(items('Apply_to_each')?['userPrincipalName'],'''','''''')}'

6) Condition: record exists?

7) YES → Update a row

8) NO → Add a new row

💡 Performance: Turn on Concurrency in “Apply to each” (e.g., 10–20) if your environment permits.

🔁 Handling Pagination (Large Tenants)

Microsoft Graph returns results in pages with an @odata.nextLink. For full sync:

  1. Start with initial GET /users

  2. If @odata.nextLink exists, loop until exhausted.

  3. Aggregate all pages into a collection (Compose/Variable/Array) and then iterate.

Simple approach: Loop with Do until on nextLink and re‑issue HTTP GET with the nextLink URL, appending the accumulated value array.

🔐 Security & Governance

💡 Notes & Best Practices