Dynamics 365

Creating application users in Dataverse

7 min read
#dynamics-365#dataverse#entra-id#managed-identity#azure-cli#pac-cli
Georg Pfeiffer
Georg Pfeiffer
Senior Solution Architect

Every non-interactive workload that talks to Dataverse needs an identity. You could hand it a real user's credentials and inherit their license cost, their MFA prompts, their password rotation schedule, and an audit trail that lies about who actually changed the data. Or you could create an application user: a non-interactive identity that costs nothing, requires no license, and gets its own independent API budget.

Dataverse supports two backing identity types for application users: Entra app registrations and user-assigned managed identities. App registrations are the classic approach -- they work from anywhere (on-premises, other clouds, CI runners), but you own a client secret that expires and can leak. Managed identities eliminate the secret entirely: Azure handles token issuance, there's nothing to rotate, nothing to store, nothing to leak. The trade-off is that your workload must run on Azure compute.

If you arrived here from the rotating application users post looking for how to actually create those users -- this is that post. We'll walk through both types manually via Power Platform Admin Center, then script the whole thing with az and pac CLI for when you need more than one or two.

When to use which

The choice is a deployment-topology decision, not a preference:

App RegistrationManaged Identity
Secret managementClient secret or certificate -- you rotate itNone -- Azure handles tokens
Works outside AzureYesNo
Setup complexityApp registration + service principal + secretOne ARM resource
Best fitCI/CD runners, on-prem servers, multi-cloudAzure Functions, App Service, Container Apps, AKS

If your workload runs on Azure compute that supports managed identity assignment, use a managed identity. No secret to store means no secret to leak, no expiry calendar to manage, no incident when someone commits a credential to source control. If it runs outside Azure -- a GitHub Actions runner, an on-premises server, another cloud -- an app registration is your only option.

Both identity types register the same way in Dataverse: as an application user with a security role. The Power Platform Admin Center flow and the pac admin assign-user command are identical for both.

Creating an app registration-backed user

Register the app in Entra ID

Open the Azure portal, navigate to Microsoft Entra ID > App registrations, and click New registration.

Give it a descriptive name that identifies the workload, not a generic "API User". Set Supported account types to Accounts in this organizational directory only (single tenant). You don't need a redirect URI. Click Register.

The completed app registration overview showing the Application (client) ID

Copy the Application (client) ID from the overview page -- you'll need it for the PPAC step.

Next, create a client secret. Go to Certificates & secrets > Client secrets > New client secret. Give it a description and an expiry. Click Add and copy the secret value immediately -- you won't see it again after you leave this page.

Client secret created with the value visible

Register as application user in PPAC

Open Power Platform Admin Center, navigate to your environment, then Settings > Users + permissions > Application users.

The Application users list in PPAC

Click New app user > Add an app. The picker shows all app registrations in your tenant. Find yours by name and select it.

Choose the Business unit (root is fine for most cases) and assign a Security role.

The create dialog with business unit and security role assigned
๐Ÿ’ก Custom security role, not System Administrator
Sysadmin works for a quick test, but in production you're handing a non-interactive identity the keys to the kingdom. Create a role with only the table privileges the integration actually needs. If you're not sure which those are, start with nothing and add them as the integration fails -- the error messages tell you exactly what's missing.

Click Create. The application user appears in the list, ready to authenticate.

The created app user in the application users list

Creating a UAMI-backed user

Create the managed identity

Open the Azure portal and search for Managed Identities. Click Create.

Pick a subscription, resource group, and region. Give it a name that identifies the workload -- something like uami-dataverse-sync-prod. Click Review + create > Create.

The created managed identity overview showing the Client ID

Copy the Client ID from the overview page. PPAC uses this to find the identity.

Register as application user in PPAC

The PPAC flow is the same as for app registrations, with one difference: managed identities don't show up in the app picker by name. You search by Client ID instead.

Navigate to your environment > Settings > Users + permissions > Application users > New app user > Add an app. Paste the Client ID into the search box.

Searching for the managed identity by Client ID in the app picker

Select the identity, assign a business unit and security role, and click Create.

The created UAMI application user in the list
โš  Entra propagation delay
A freshly created managed identity may not appear in the PPAC picker for a minute or two. If the search returns nothing, wait and try again. The scripted approach below handles this with an automatic retry loop.

Scripting with az and pac CLI

The manual flow is fine for a handful of users. When you need a pool -- for instance, to rotate them under API throttling -- scripting saves the clicking. Both scripts below use az for Entra and ARM operations and pac for the Dataverse registration.

โ„น Prerequisites
`az` must be logged in (`az login`) with permissions to create app registrations or managed identities. `pac` must be authenticated against your Power Platform environment (`pac auth create`). The bash variants use `jq` for JSON parsing.

App registration

The script creates an Entra app registration, a service principal, a client secret, and registers the result as an application user in Dataverse -- the same four steps from the manual walkthrough, end to end.

#!/bin/bash
APP_NAME="Dataverse API User 01"
ENV_URL="https://yourorg.crm4.dynamics.com"
ROLE_NAME="Integration User"

# create entra app registration
APP_ID=$(az ad app create \
  --display-name "$APP_NAME" \
  --sign-in-audience AzureADMyOrg \
  --query appId -o tsv)

# create service principal
az ad sp create --id "$APP_ID" --output none

# generate client secret (1 year)
CREDS=$(az ad app credential reset \
  --id "$APP_ID" \
  --display-name "dataverse" \
  --years 1)

SECRET=$(echo "$CREDS" | jq -r '.password')
TENANT=$(echo "$CREDS" | jq -r '.tenant')

echo "Client ID: $APP_ID"
echo "Secret:    $SECRET"
echo "Tenant:    $TENANT"

# register as application user in Dataverse
pac admin assign-user \
  --environment "$ENV_URL" \
  --user "$APP_ID" \
  --role "$ROLE_NAME" \
  --application-user
$AppName = "Dataverse API User 01"
$EnvUrl = "https://yourorg.crm4.dynamics.com"
$RoleName = "Integration User"

# create entra app registration
$AppId = az ad app create `
  --display-name $AppName `
  --sign-in-audience AzureADMyOrg `
  --query appId -o tsv

# create service principal
az ad sp create --id $AppId --output none

# generate client secret (1 year)
$Creds = az ad app credential reset `
  --id $AppId `
  --display-name "dataverse" `
  --years 1 | ConvertFrom-Json

$Secret = $Creds.password
$Tenant = $Creds.tenant

Write-Host "Client ID: $AppId"
Write-Host "Secret:    $Secret"
Write-Host "Tenant:    $Tenant"

# register as application user in Dataverse
pac admin assign-user `
  --environment $EnvUrl `
  --user $AppId `
  --role $RoleName `
  --application-user
โš  Secret in the terminal
The client secret is printed to stdout. In a real pipeline, write it directly to a key vault instead of echoing it.

Managed identity

Same idea, fewer moving parts. The script creates a user-assigned managed identity and registers it in Dataverse. The one wrinkle: a freshly created UAMI needs a moment to propagate through Entra before pac can find it, so the script retries the registration in a loop.

#!/bin/bash
IDENTITY_NAME="uami-dataverse-api-001"
RESOURCE_GROUP="rg-integrations"
LOCATION="westeurope"
ENV_URL="https://yourorg.crm4.dynamics.com"
ROLE_NAME="Integration User"

# create user-assigned managed identity
UAMI=$(az identity create \
  --name "$IDENTITY_NAME" \
  --resource-group "$RESOURCE_GROUP" \
  --location "$LOCATION")

CLIENT_ID=$(echo "$UAMI" | jq -r '.clientId')

echo "Managed Identity: $IDENTITY_NAME"
echo "Client ID:        $CLIENT_ID"

# register as application user in Dataverse (retry until Entra propagation completes)
registered=0
for attempt in $(seq 1 12); do
  if pac admin assign-user \
    --environment "$ENV_URL" \
    --user "$CLIENT_ID" \
    --role "$ROLE_NAME" \
    --application-user 2>/dev/null; then
    registered=1
    break
  fi
  echo "  attempt $attempt/12 - not yet available, retrying in 10s..."
  sleep 10
done

if [ "$registered" -ne 1 ]; then
  echo "Failed to create app user after 2 minutes" >&2
  exit 1
fi
$IdentityName = "uami-dataverse-api-001"
$ResourceGroup = "rg-integrations"
$Location = "westeurope"
$EnvUrl = "https://yourorg.crm4.dynamics.com"
$RoleName = "Integration User"

# create user-assigned managed identity
$Uami = az identity create `
  --name $IdentityName `
  --resource-group $ResourceGroup `
  --location $Location | ConvertFrom-Json

$ClientId = $Uami.clientId

Write-Host "Managed Identity: $IdentityName"
Write-Host "Client ID:        $ClientId"

# register as application user in Dataverse (retry until Entra propagation completes)
for ($attempt = 1; $attempt -le 12; $attempt++) {
    $output = pac admin assign-user `
      --environment $EnvUrl `
      --user $ClientId `
      --role $RoleName `
      --application-user 2>&1
    if ($LASTEXITCODE -eq 0) { Write-Host $output; break }
    Write-Host "  attempt $attempt/12 - not yet available, retrying in 10s..."
    Start-Sleep -Seconds 10
}
if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create app user after 2 minutes"; exit 1 }
๐Ÿ’ก Bulk creation
Wrap either script in a loop with a parameterized name (`Dataverse API User 01` through `05`, or `uami-dataverse-api-001` through `005`) to create a pool of application users for [user rotation](/blog/scaling-past-dataverse-api-limits). Each user is its own API budget, and the cost of claiming it is one run of the script above.

Wrap-up

Two identity types, same result: a non-interactive Dataverse user with its own API budget and no license cost. App registrations work everywhere but saddle you with a secret to rotate. Managed identities eliminate the secret entirely when your workload runs on Azure. Either way, the scripts above get you from zero to a registered application user in under a minute -- and when you need a pool of them, you're one for loop away from the user rotation pattern that turns a per-user API limit into a multiplier.

Share this article

Georg Pfeiffer

About the Author

Georg is a senior solution architect specializing in .NET, Azure, and Dynamics 365. He helps organizations design and build scalable, maintainable enterprise systems. When he's not writing code, he's writing about it here.

Learn more about Georg