Generating a Microsoft Graph Bearer Token in Power Automate Desktop (App ID + Secret)
Power Automate Desktop has no native Microsoft Graph connector. If you want a desktop flow to create users, read directories, send mail, or touch SharePoint through Graph, you have to do what every daemon application does: acquire an app-only access token with the OAuth 2.0 client credentials grant, then attach it as a Bearer header on every call.
This post walks through a reusable GetGraphToken subflow that does exactly that — including the three configuration details that silently break it, and a validation bug that is very easy to write backwards.
1. What we're building
| Component | Purpose |
|---|---|
| Entra ID app registration | Identity of the automation (client ID + secret) |
Main flow | Sets configuration variables, calls the token subflow |
GetGraphToken subflow | Reads the secret, POSTs to the token endpoint, parses and validates the response |
AuthHeader variable | Bearer <token> string reused by every subsequent Graph call |
Keeping token acquisition in its own subflow means every Graph action in the flow calls one place, and token caching becomes a single change instead of ten.
2. The protocol, per Microsoft
Client credentials is a two-legged flow: permissions are granted directly to the application by an administrator, and there is no user in the picture. Microsoft's guidance is worth quoting on the shape of the request.
Token request — POST to the tenant-specific v2.0 token endpoint:
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id={client-id}
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_secret={secret-value}
&grant_type=client_credentials
| Parameter | Required | Notes |
|---|---|---|
tenant | Yes | GUID or domain name (e.g. contoso.onmicrosoft.com) |
client_id | Yes | The Application (client) ID from the app registration |
scope | Yes | Resource identifier suffixed with /.default. For Graph: https://graph.microsoft.com/.default. All scopes in one request must belong to a single resource |
client_secret | Yes | The secret Value, not the Secret ID. Microsoft's docs state it must be URL-encoded before being sent |
grant_type | Yes | Literal client_credentials |
Successful response:
{
"token_type": "Bearer",
"expires_in": 3599,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs..."
}
expires_in is in seconds. There is no refresh token in this flow — when the token expires you simply call /token again.
Error response is HTTP 400 with a JSON body containing error, error_description, error_codes, trace_id, and correlation_id. The error_description carries the AADSTS code you'll actually debug from.
Note on the
/.defaultscope: it tells the identity platform to issue a token for whichever application permissions have already been consented for that resource. It does not grant anything. If admin consent hasn't been given, you still get a token — it just comes back without the roles you need, and Graph answers403.
3. App registration checklist
| Step | Where | Detail |
|---|---|---|
| Register app | Entra admin center → App registrations → New registration | Single tenant is fine for internal automation |
| Copy identifiers | Overview blade | Application (client) ID, Directory (tenant) ID |
| Add permissions | API permissions → Microsoft Graph → Application permissions | Delegated permissions do not work here — there is no user to act on behalf of |
| Grant consent | API permissions → Grant admin consent | Required. Without it the token is issued but carries no roles claim |
| Create secret | Certificates & secrets → New client secret | Copy the Value immediately; it is never shown again |
Client secret lifetime: Microsoft caps portal-created secrets at 24 months and explicitly recommends setting expiry to less than 12 months. Secrets expire without any built-in notification, so put the expiry date in a calendar or build an expiry report — a silently expired secret is one of the most common causes of "the robot stopped working overnight."
For anything production-grade, prefer a certificate credential or workload identity federation over a shared secret. Both are supported by the same client credentials grant; only the client authentication portion of the request changes.
4. Main flow: configuration variables
Keep configuration out of the subflow so the subflow stays reusable.
SET TenantId TO $'''<your-tenant-id-or-domain>'''
SET ClientId TO $'''<your-application-client-id>'''
SET TokenUrl TO $'''https://login.microsoftonline.com/%TenantId%/oauth2/v2.0/token'''
SET GraphBase TO $'''https://graph.microsoft.com/v1.0'''
SET GraphScope TO $'''https://graph.microsoft.com/.default'''
SET LabRoot TO $'''C:\GraphLab'''
CALL GetGraphToken
Two things to note:
- The tenant ID and client ID are not secrets, but they are environment-specific. If the flow moves between dev and prod, promote them to desktop flow input variables rather than hardcoding them.
%TenantId%inside the URL works because PAD evaluates variables inside$'''...'''text literals. If your URL ever contains a literal%, escape it as%%.
5. Where the secret lives
Never hardcode the secret in a Set variable action. The flow definition is stored in Dataverse and is visible to anyone who can open the flow. Three workable options:
| Approach | How it works | Trade-off |
|---|---|---|
| Windows Credential Manager | Run PowerShell script reads the secret from the local vault | Simple; per-machine and per-user, so it doesn't travel with the flow. Requires a third-party PowerShell module |
| Azure Key Vault via a parent cloud flow | Cloud flow uses the Key Vault Get secret action, passes it into the desktop flow as an input variable | Microsoft's documented pattern; centralised rotation. Requires a cloud flow and Key Vault access |
| Certificate credential | App authenticates with a signed JWT assertion instead of a secret | Highest assurance, no secret to leak. More work to implement in PAD |
Option A — Credential Manager
The Get-StoredCredential cmdlet comes from the community CredentialManager module on the PowerShell Gallery (MIT-licensed, originally by Dave Garnar). It is not shipped with Windows, so it must be installed on every machine that runs the flow:
Install-Module CredentialManager -Scope AllUsers
New-StoredCredential -Target 'GraphAutomation' -UserName 'app' `
-Password '<secret-value>' -Type Generic -Persist LocalMachine
Microsoft's own Microsoft.PowerShell.SecretManagement + SecretStore modules are an alternative, but note the PowerShell team has declared them feature-complete and no longer actively developed (security and critical bug fixes only), so weigh that before standardising on them.
Option B — Key Vault via cloud flow
Microsoft's RPA guidance documents this end to end: the cloud flow calls Azure Key Vault → Get secret with Secure Outputs enabled, then calls the desktop flow with Secure Inputs enabled. On the PAD side, define an input variable and toggle Mark as sensitive so the value is masked in both cloud and desktop run logs.
This is the option to reach for if the flow runs unattended on a machine group.
6. The GetGraphToken subflow
6.1 Read and URL-encode the secret
PAD's Run PowerShell script action starts an instance of powershell.exe and passes the script as an argument — that's Windows PowerShell, not PowerShell 7, unless your PATH resolves powershell.exe elsewhere.
Scripting.RunPowershellScript.RunScript Script: $'''$ErrorActionPreference = \'Stop\'
Import-Module CredentialManager -ErrorAction Stop
$cred = Get-StoredCredential -Target \"GraphAutomation\"
if ($cred) {
$plain = $cred.GetNetworkCredential().Password
[uri]::EscapeDataString($plain)
} else {
throw \"Credential \'GraphAutomation\' not found in Credential Manager.\"
}''' ScriptOutput=> PSSecretOutput ScriptError=> PSSecretError
Text.Trim Text: PSSecretOutput TrimOption: Text.TrimOption.Both TrimmedText=> PSSecretOutput
IF IsNotBlank(PSSecretError) THEN
Display.ShowMessageDialog.ShowMessage Message: $'''Secret retrieval failed: %PSSecretError%''' Icon: Display.Icon.Error Buttons: Display.Buttons.OK DefaultButton: Display.DefaultButton.Button1 IsTopMost: True ButtonPressed=> ButtonPressed
EXIT FUNCTION
END
Two deliberate additions over the naive version:
Capture ScriptError. Run PowerShell script produces two outputs: ScriptOutput (standard output) and ScriptError (Write-Error, runtime errors, syntax errors). If you only bind ScriptOutput and the credential is missing, PSSecretOutput comes back empty and the flow marches on to build a malformed request body — you then debug the token endpoint instead of the vault.
Encode the secret in PowerShell. [uri]::EscapeDataString() percent-encodes the value. This matters because of the next section.
Always trim. PowerShell output arrives with a trailing newline. An untrimmed secret produces AADSTS7000215 and a long afternoon.
6.2 The token request — and the setting that breaks everything
Web.InvokeWebService.InvokeWebServicePost Url: TokenUrl Accept: $'''application/json''' ContentType: $'''application/x-www-form-urlencoded''' RequestBody: $'''grant_type=client_credentials&client_id=%ClientId%&client_secret=%PSSecretOutput%&scope=%GraphScope%''' ConnectionTimeout: 30 FollowRedirection: True ClearCookies: False FailOnErrorStatus: False EncodeRequestBody: False UserAgent: $'''Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.21) Gecko/20100312 Firefox/3.6''' Encoding: Web.Encoding.utf_8 AcceptUntrustedCertificates: False TrimRequestBody: True Response=> TokenResponse StatusCode=> TokenStatusCode
ON ERROR REPEAT 5 TIMES WAIT 3
END
| Setting | Value | Why |
|---|---|---|
| Content type | application/x-www-form-urlencoded | The token endpoint rejects JSON |
| Encode request body (Advanced) | Off | The single most common failure. Leave it on and PAD encodes the entire body — including the & and = separators — so the endpoint sees one opaque blob and replies AADSTS7000218: The request body must contain the following parameter: 'grant_type' |
| Fail on error status | Off | Lets you read the 400 body and its AADSTS code instead of getting a bare PAD exception |
| Follow redirection | On/Off | Either works for this endpoint; several community write-ups turn it off as a precaution |
| Connection timeout | 30 | Reasonable default |
Because Encode request body must be off, PAD will not percent-encode the secret for you — which is why we did it in PowerShell in step 6.1. Entra secret values regularly contain characters like ~, ., -, and _ that are safe in a form body, but they can also contain characters that are not. Encoding it once, at the source, removes the class of bug entirely.
On the retry policy: ON ERROR REPEAT 5 TIMES WAIT 3 is good practice, but be aware that with Fail on error status = Off, a 400 or 401 is not an error as far as PAD is concerned. The retry only covers transport-level failures — DNS, TLS, timeouts. That's actually what you want: retrying an invalid secret five times just delays the inevitable.
6.3 Parse the response
Variables.ConvertJsonToCustomObject Json: TokenResponse CustomObject=> TokenObject
SET AccessToken TO TokenObject['access_token']
SET AuthHeader TO $'''Bearer %AccessToken%'''
DateTime.GetCurrentDateTime.Local DateTimeFormat: DateTime.DateTimeFormat.DateAndTime CurrentDateTime=> CurrentDateTime
DateTime.Add DateTime: CurrentDateTime TimeToAdd: TokenObject['expires_in'] - 300 TimeUnit: DateTime.TimeUnit.Seconds ResultedDate=> TokenExpiry
The 300-second subtraction is a safety margin: the token is treated as stale five minutes before it actually expires, so a long-running Graph loop never hands over a token that dies mid-request.
7. The validation bug worth fixing
This is the pattern I see most often, and it reads perfectly plausibly until you trace it:
# INVERTED — do not use
IF TokenExpiry > CurrentDateTime THEN
IF IsNotBlank(AccessToken) THEN
Display.ShowMessageDialog.ShowMessage Message: $'''Token acquisition failed...'''
EXIT FUNCTION
END
END
Read it literally: if the token is still valid, and the token is not blank, declare failure and exit. The conditions describe the success case. On a genuine failure — blank AccessToken, TokenExpiry in the past — both conditions are false, nothing fires, and the subflow returns cleanly with an empty AuthHeader. Every downstream Graph call then fails with 401, and you go hunting for a permissions problem that doesn't exist.
The correct version validates on the status code first, then the payload:
IF TokenStatusCode <> 200 THEN
Display.ShowMessageDialog.ShowMessage Message: $'''Token acquisition failed. Status: %TokenStatusCode%. Response: %TokenResponse%''' Icon: Display.Icon.Error Buttons: Display.Buttons.OK DefaultButton: Display.DefaultButton.Button1 IsTopMost: True ButtonPressed=> ButtonPressed
EXIT FUNCTION
END
IF IsBlank(AccessToken) THEN
Display.ShowMessageDialog.ShowMessage Message: $'''Token endpoint returned 200 but no access_token was present.''' Icon: Display.Icon.Error Buttons: Display.Buttons.OK DefaultButton: Display.DefaultButton.Button1 IsTopMost: True ButtonPressed=> ButtonPressed
EXIT FUNCTION
END
Guard clauses beat nested IFs here. Each condition describes one failure, exits immediately, and reads the way you'd describe it out loud.
For unattended runs, replace the message dialogs with a Write text to file logging action or a Set variable that the parent flow inspects — a modal dialog on an unattended machine hangs the run until the session times out.
8. Token caching
Since the token is valid for roughly an hour, don't request a new one for every Graph call. Put the cache check at the very top of the subflow:
DateTime.GetCurrentDateTime.Local DateTimeFormat: DateTime.DateTimeFormat.DateAndTime CurrentDateTime=> CurrentDateTime
IF IsNotBlank(AccessToken) AND TokenExpiry > CurrentDateTime THEN
EXIT FUNCTION
END
# ... otherwise fall through and acquire a fresh token
Now every Graph action can call GetGraphToken unconditionally, and the subflow decides whether a network round trip is actually needed. Initialise AccessToken to an empty string in Main so the first check evaluates correctly.
9. Using the token
Web.InvokeWebService.InvokeWebService Url: $'''%GraphBase%/users?$top=10''' Method: Web.Method.Get Accept: $'''application/json''' ContentType: $'''application/json''' CustomHeaders: $'''Authorization: %AuthHeader%''' ConnectionTimeout: 30 FollowRedirection: True ClearCookies: False FailOnErrorStatus: False Encoding: Web.Encoding.utf_8 Response=> GraphResponse StatusCode=> GraphStatusCode
Variables.ConvertJsonToCustomObject Json: GraphResponse CustomObject=> GraphResult
AuthHeader already contains the Bearer prefix, so the custom header line is Authorization: %AuthHeader% — not Authorization: Bearer %AuthHeader%. Doubling the prefix is a classic 401.
Also note the $ in $top — PAD treats $'''...''' as a text literal, and $top inside it is fine, but if you build the URL through concatenation, watch for % characters in OData filters that need escaping as %%.
Microsoft's warning is worth repeating: don't decode or validate the token in your own code. Tokens for Microsoft services may use formats that won't parse as a standard JWT. If you want to confirm which roles were granted, make a real Graph call and read the response.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret' | Encode request body left On, or the body is malformed | Turn it Off in Advanced settings |
AADSTS7000215: Invalid client secret is provided | Secret ID pasted instead of secret Value; trailing whitespace; unencoded special characters | Re-copy the Value; trim; percent-encode |
AADSTS7000222: The provided client secret keys are expired | Secret past its expiry date | Create a new secret, or move to certificate credentials |
AADSTS700016: Application with identifier '...' was not found in the directory | Wrong client ID, or right client ID against the wrong tenant | Verify both GUIDs come from the same app registration |
AADSTS70011: The provided value for the input parameter 'scope' is not valid | Malformed scope, or scopes from more than one resource | Use exactly https://graph.microsoft.com/.default |
Token issued (200) but Graph returns 403 Insufficient privileges | Application permission added but admin consent never granted | Grant admin consent in the API permissions blade |
Token issued but Graph returns 401 | Bearer prefix doubled, or AuthHeader empty because validation was inverted | Check section 7 |
Failed to run PowerShell script — the system cannot find the file specified | powershell.exe not on the PATH | Add C:\WINDOWS\System32\WindowsPowerShell\v1.0\ to the system PATH (confirm with $PsHome) |
| Everything works interactively, fails unattended | Credential Manager entry is per-user; or a modal dialog is blocking | Use Persist LocalMachine, or switch to the Key Vault input-variable pattern; replace dialogs with file logging |
A fast way to isolate PAD from Entra: reproduce the exact same POST in a REST client. If it succeeds there and fails in PAD, the problem is in the action's Advanced settings — nine times out of ten, Encode request body.
11. Before you ship
- Delete every debug
Display messageaction that renders the secret or the token, including disabled ones - Secret is retrieved at runtime, never stored in a
Set variableaction - Tenant ID and client ID are input variables, not hardcoded constants
- Application permissions follow least privilege (
User.Read.AlloverDirectory.ReadWrite.Allwhere it suffices) - Admin consent granted and recorded
- Secret expiry tracked, with a rotation owner named
- Failure paths write to a log file rather than a modal dialog
- Token cache check is at the top of the subflow
- Validation guard clauses tested by deliberately breaking the secret
12. Complete Robin code
Everything above, assembled. PAD stores each subflow separately, so this is presented as two blocks — select the actions in the code view of the matching tab and paste.
The FUNCTION ... END FUNCTION wrapper is deliberately omitted: you paste action bodies into an existing subflow tab, not the wrapper. Create the subflow first (Subflows → New subflow, name it GetGraphToken), then paste block 2 into it.
Block 1 — Main
# ---------- Configuration ----------
SET TenantId TO $'''<your-tenant-id-or-domain>'''
SET ClientId TO $'''<your-application-client-id>'''
SET CredentialTarget TO $'''GraphAutomation'''
SET TokenUrl TO $'''https://login.microsoftonline.com/%TenantId%/oauth2/v2.0/token'''
SET GraphBase TO $'''https://graph.microsoft.com/v1.0'''
SET GraphScope TO $'''https://graph.microsoft.com/.default'''
SET LabRoot TO $'''C:\GraphLab'''
SET LogFile TO $'''%LabRoot%\graph-automation.log'''
# ---------- Token cache seed ----------
# AccessToken starts blank and TokenExpiry starts in the past so the
# subflow's cache check always misses on the first call.
SET AccessToken TO $''''''
SET AuthHeader TO $''''''
DateTime.GetCurrentDateTime.Local DateTimeFormat: DateTime.DateTimeFormat.DateAndTime CurrentDateTime=> SeedDateTime
DateTime.Add DateTime: SeedDateTime TimeToAdd: -1 TimeUnit: DateTime.TimeUnit.Days ResultedDate=> TokenExpiry
Folder.Create FolderPath: $'''%LabRoot%''' FolderName: $'''logs''' Folder=> NewFolder
ON ERROR
END
# ---------- Acquire token ----------
CALL GetGraphToken
IF IsBlank(AuthHeader) THEN
EXIT
END
# ---------- Sample Graph call ----------
Web.InvokeWebService.InvokeWebService Url: $'''%GraphBase%/users?$top=10''' Method: Web.Method.Get Accept: $'''application/json''' ContentType: $'''application/json''' CustomHeaders: $'''Authorization: %AuthHeader%''' ConnectionTimeout: 30 FollowRedirection: True ClearCookies: False FailOnErrorStatus: False Encoding: Web.Encoding.utf_8 Response=> GraphResponse StatusCode=> GraphStatusCode
ON ERROR REPEAT 3 TIMES WAIT 3
END
IF GraphStatusCode <> 200 THEN
File.WriteText File: LogFile TextToWrite: $'''[%CurrentDateTime%] Graph call failed. Status: %GraphStatusCode%. Body: %GraphResponse%''' AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
EXIT
END
Variables.ConvertJsonToCustomObject Json: GraphResponse CustomObject=> GraphResult
LOOP FOREACH CurrentUser IN GraphResult['value']
File.WriteText File: LogFile TextToWrite: $'''[%CurrentDateTime%] %CurrentUser['displayName']% - %CurrentUser['userPrincipalName']%''' AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
END
Block 2 — GetGraphToken subflow
# ---------- 1. Cache check ----------
DateTime.GetCurrentDateTime.Local DateTimeFormat: DateTime.DateTimeFormat.DateAndTime CurrentDateTime=> CurrentDateTime
IF IsNotBlank(AccessToken) THEN
IF TokenExpiry > CurrentDateTime THEN
EXIT FUNCTION
END
END
# ---------- 2. Read the secret from Credential Manager ----------
Scripting.RunPowershellScript.RunScript Script: $'''$ErrorActionPreference = \'Stop\'
Import-Module CredentialManager -ErrorAction Stop
$cred = Get-StoredCredential -Target \"%CredentialTarget%\"
if ($cred) {
$plain = $cred.GetNetworkCredential().Password
[uri]::EscapeDataString($plain)
} else {
throw \"Credential \'%CredentialTarget%\' not found in Credential Manager.\"
}''' ScriptOutput=> PSSecretOutput ScriptError=> PSSecretError
Text.Trim Text: PSSecretOutput TrimOption: Text.TrimOption.Both TrimmedText=> PSSecretOutput
IF IsNotBlank(PSSecretError) THEN
File.WriteText File: LogFile TextToWrite: $'''[%CurrentDateTime%] Secret retrieval failed: %PSSecretError%''' AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
SET AuthHeader TO $''''''
EXIT FUNCTION
END
IF IsBlank(PSSecretOutput) THEN
File.WriteText File: LogFile TextToWrite: $'''[%CurrentDateTime%] Secret retrieval returned an empty value for target %CredentialTarget%.''' AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
SET AuthHeader TO $''''''
EXIT FUNCTION
END
# ---------- 3. Request the token ----------
# Advanced settings: Encode request body = OFF, Fail on error status = OFF
Web.InvokeWebService.InvokeWebServicePost Url: TokenUrl Accept: $'''application/json''' ContentType: $'''application/x-www-form-urlencoded''' RequestBody: $'''grant_type=client_credentials&client_id=%ClientId%&client_secret=%PSSecretOutput%&scope=%GraphScope%''' ConnectionTimeout: 30 FollowRedirection: True ClearCookies: False FailOnErrorStatus: False EncodeRequestBody: False UserAgent: $'''Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.21) Gecko/20100312 Firefox/3.6''' Encoding: Web.Encoding.utf_8 AcceptUntrustedCertificates: False TrimRequestBody: True Response=> TokenResponse StatusCode=> TokenStatusCode
ON ERROR REPEAT 5 TIMES WAIT 3
END
# Drop the secret from memory as soon as the request is sent
SET PSSecretOutput TO $''''''
# ---------- 4. Validate (guard clauses, not nested IFs) ----------
IF TokenStatusCode <> 200 THEN
File.WriteText File: LogFile TextToWrite: $'''[%CurrentDateTime%] Token acquisition failed. Status: %TokenStatusCode%. Response: %TokenResponse%''' AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
SET AccessToken TO $''''''
SET AuthHeader TO $''''''
EXIT FUNCTION
END
Variables.ConvertJsonToCustomObject Json: TokenResponse CustomObject=> TokenObject
ON ERROR
File.WriteText File: LogFile TextToWrite: $'''[%CurrentDateTime%] Token response was not valid JSON: %TokenResponse%''' AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
SET AuthHeader TO $''''''
EXIT FUNCTION
END
SET AccessToken TO TokenObject['access_token']
IF IsBlank(AccessToken) THEN
File.WriteText File: LogFile TextToWrite: $'''[%CurrentDateTime%] Token endpoint returned 200 but no access_token was present.''' AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
SET AuthHeader TO $''''''
EXIT FUNCTION
END
# ---------- 5. Build header and expiry ----------
SET AuthHeader TO $'''Bearer %AccessToken%'''
DateTime.Add DateTime: CurrentDateTime TimeToAdd: TokenObject['expires_in'] - 300 TimeUnit: DateTime.TimeUnit.Seconds ResultedDate=> TokenExpiry
File.WriteText File: LogFile TextToWrite: $'''[%CurrentDateTime%] Token acquired. Valid until %TokenExpiry%.''' AppendNewLine: True IfFileExists: File.IfFileExists.Append Encoding: File.FileEncoding.UTF8
Notes on the assembled version
| Change vs. the interactive version | Reason |
|---|---|
All Display message actions replaced with Write text to file | A modal dialog hangs an unattended run until the session times out |
Cache check written as two nested IFs rather than A AND B | Avoids relying on short-circuit evaluation when TokenExpiry may be unset |
AuthHeader cleared on every failure path | Gives Main a single, unambiguous thing to test before making Graph calls |
PSSecretOutput blanked after the POST | Keeps the secret out of the variable pane and out of any later flow-state dump |
Convert JSON to custom object wrapped in ON ERROR | A non-JSON body (proxy interception page, WAF block) otherwise throws an unhandled exception |
Folder.Create wrapped in a bare ON ERROR | Swallows the "already exists" error on subsequent runs |
Replace the <your-tenant-id-or-domain> and <your-application-client-id> placeholders, and create the Credential Manager entry with the target name matching CredentialTarget before the first run.
References
- OAuth 2.0 client credentials flow on the Microsoft identity platform
- Add and manage app credentials in Microsoft Entra ID
- Microsoft Entra authentication and authorization error codes
- Power Automate — HTTP actions reference
- Power Automate — Scripting actions reference
- "Failed to run PowerShell script" troubleshooting
- Secure data used in cloud flows
- Creating a cloud flow to launch a desktop flow (Key Vault pattern)
No comments:
Post a Comment