Power Automate logs every action's full inputs and outputs to run history by default — including bearer tokens, API keys, and PII. This expert lesson teaches you how to implement Secure Input and Output masking strategically, architect flows to prevent sensitive data from leaking across action boundaries, and enforce masking compliance programmatically through CI/CD pipeline linting and CoE Toolkit auditing.

Picture this: your organization's Power Automate flow successfully retrieves an OAuth bearer token from a third-party API, passes it through a few HTTP actions, and sends a notification. Everything works perfectly. Then someone — maybe an auditor, maybe a curious colleague with Flow Owner permissions, maybe a junior developer troubleshooting a timeout — opens the run history for that flow. In the action inputs panel, sitting plainly in JSON, is the full bearer token your flow used 47 minutes ago. It's still valid. It grants access to production data. And it's been visible to everyone with read access to that flow for the past six weeks.
This is not a hypothetical. Run history exposure is one of the most underappreciated security gaps in Power Automate production environments. Unlike a misconfigured permission or a leaked environment variable, it's insidious because the data isn't technically "leaked" — it's sitting exactly where Power Automate put it, in the run history UI that you use every day for debugging. The problem is that Power Automate logs inputs and outputs at every action by default, and without deliberate intervention, your flows are writing a detailed audit trail of every credential, PII field, session token, and API key that passes through them.
By the end of this article, you'll know exactly how secure input and output masking works at the platform level, when and how to apply it strategically, how to structure flows so that sensitive data never enters an action's logged surface at all, and how to verify that your masking is actually working. We'll also cover the compliance and governance implications that make this topic non-negotiable for any enterprise dealing with regulated data.
What you'll learn:
You should be comfortable building and deploying cloud flows in Power Automate, including solution-aware flows. This article assumes you understand connection references, HTTP actions, environment variables, and basic flow governance. If you're newer to secrets management patterns, read Integrating Power Automate with Azure Key Vault and Managed Identities first — it provides essential context for why run history masking and secrets retrieval must be designed together. Familiarity with Power Platform DLP policies is also assumed.
Before you can protect sensitive data in run history, you need to understand what "run history" actually is under the hood. When a flow executes, the Power Automate runtime captures a structured record of every action's execution. This record includes:
These records are stored in Microsoft's backend infrastructure and surfaced through the flow run history UI in the Power Automate portal. Critically, they are also accessible programmatically through the Power Automate Management API and through the Power Platform Admin APIs — which means they're accessible to anyone who can query those APIs with appropriate permissions, not just people using the portal UI.
The retention window for run history is 28 days by default for standard cloud flows, and 90 days for premium flows with extended logging enabled. That's a long exposure window for a bearer token or a customer's Social Security Number.
Warning
Run history is accessible to everyone with the "Owner" or "Run-only user" access to a flow, depending on how sharing is configured. If your flow has been shared broadly for operational reasons, you may have many more people with visibility into run history inputs and outputs than you realize. Always audit flow sharing as part of your security posture review.
What makes this particularly dangerous in practice is the nature of how data flows through actions. Consider an HTTP action that calls an HR API. The action's input panel in run history will display the full request, including:
{
"method": "GET",
"uri": "https://hrapi.internal.example.com/v2/employees/E10482",
"headers": {
"Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"Content-Type": "application/json",
"X-Api-Key": "prod-sk-a7f3c82d-9b14-4e6a-bc01-d5f221a8e3c4"
}
}
And the output panel will contain the full response body:
{
"statusCode": 200,
"body": {
"employeeId": "E10482",
"name": "Maria Castellanos",
"ssn": "XXX-XX-4829",
"salary": 127500,
"bankAccountNumber": "00428847192"
}
}
Both the bearer token and the PII response are now sitting in run history for 28 days, accessible to anyone with appropriate permissions on this flow. This is the default behavior. You have to opt out of it deliberately.
Power Automate provides two settings on every action and trigger: Secure Inputs and Secure Outputs. When enabled, these settings cause the runtime to replace the logged content with a placeholder message instead of the actual data.
The placeholder looks like this in the run history UI:
Content not shown due to security configuration.
And in the raw API response when querying run history programmatically, the inputs or outputs object will be replaced with a masked sentinel value. The actual data is never written to the run history store when these flags are enabled — this is not redaction after the fact, it's suppression at write time. That's an important distinction: you're not relying on a scrubbing process to remove sensitive data; the data simply never enters the log.
Enabling Secure Inputs and Secure Outputs is done through the action settings panel:
The toggle is available for virtually every action type: HTTP, Send an Email, Compose, Parse JSON, Apply to Each, and all connector actions. The same settings are available on the trigger, which is important because trigger payloads — including webhook request bodies — are also logged by default.
Key insight
Secure Inputs and Secure Outputs are independent toggles. You should evaluate each axis separately for every action. An HTTP action that sends credentials in its request but only receives a success/failure status code in its response needs Secure Inputs enabled, but Secure Outputs may be left transparent for debugging purposes. Conversely, an action that receives a response containing PII but whose inputs are innocuous needs Secure Outputs enabled, but Secure Inputs can stay off.
Here is where most developers get caught out: masking only applies to the action it's configured on. If a masked action's output is passed to a downstream action — say, you extract a token from a masked HTTP response and pass it into a Compose action — the Compose action will log that token in its inputs, completely unmasked.
This is not a bug. It's working exactly as designed. The platform can't know which parts of a downstream action's inputs came from a masked source. You are responsible for propagating masking discipline throughout the flow. We'll cover the architectural patterns for doing this correctly later in this article.
Understanding what these settings look like in the underlying flow definition is essential for enterprise ALM workflows. When you enable Secure Inputs or Secure Outputs on an action through the UI, the flow saves this as metadata in the action's JSON definition inside the flow package.
Here's what an HTTP action looks like with both secure flags enabled in the flow definition JSON:
"Get_OAuth_Token": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://auth.example.com/oauth2/token",
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"body": "grant_type=client_credentials&client_id=@{variables('clientId')}&client_secret=@{variables('clientSecret')}&scope=api://hrapi.internal.example.com/.default"
},
"metadata": {
"operationMetadataId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
},
"runtimeConfiguration": {
"secureData": {
"properties": [
"inputs",
"outputs"
]
}
}
}
The critical section is runtimeConfiguration.secureData.properties. This array controls what gets masked. Valid values are "inputs", "outputs", or both. When deploying flows as part of a solution package through your ALM pipeline, you can inspect and enforce these settings programmatically before promoting a solution to production.
Tip
Add a pre-deployment validation step to your Azure DevOps pipeline or GitHub Actions workflow that parses the flow definition JSON and checks that actions handling known sensitive data types (those tagged with keywords like "token," "secret," "key," "password," "ssn," "creditcard") have the appropriate secureData properties set. This creates an automated compliance gate rather than relying on manual review.
When you export a solution and inspect the flow definition inside the .zip package, you'll find the flow JSON at Workflows/{FlowName}/definition.json. This is the artifact you should be linting in CI/CD. For context on how solution export/import works in ALM pipelines, see Deploying and Managing Power Automate Solutions Across Environments.
Don't neglect the trigger. If your flow is triggered by an HTTP request that includes sensitive data — a webhook payload containing a customer record, a form submission with PII, a service-to-service call with authentication headers — the trigger's inputs are logged exactly like any other action.
The trigger definition in JSON looks like this with secure inputs enabled:
"triggers": {
"manual": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {
"type": "object",
"properties": {
"employeeId": { "type": "string" },
"taxId": { "type": "string" },
"accountNumber": { "type": "string" }
}
}
},
"runtimeConfiguration": {
"secureData": {
"properties": [
"inputs"
]
}
}
}
}
Note that triggers only support "inputs" in the secureData properties array (since triggers don't produce outputs in the same way actions do — they receive the triggering payload as their "input"). Attempting to add "outputs" to a trigger's secureData has no effect on most trigger types.
Enabling Secure Inputs and Secure Outputs is necessary but not sufficient. The real challenge is preventing sensitive data from leaking into actions where you haven't enabled masking — and doing so in a way that doesn't make your flows unmaintainable.
The most powerful architectural pattern is isolating all sensitive data operations inside a Scope action with secure settings enabled. A Scope in Power Automate is a logical container for a group of actions. You can configure the entire Scope's inputs and outputs as secure, and all actions nested inside inherit that context.
Here's how to structure a "Token Retrieval Scope":
[SECURE] OAuth Token Retrieval.The flow variable itself doesn't appear in run history (variables don't have their own run history entries). Only the actions that read or write those variables do. So if you set a variable inside a masked Scope, and then pass that variable to an HTTP action in the main flow body, you need to ensure that HTTP action also has Secure Inputs enabled, because otherwise the bearer token will appear in that action's input log even though it was set inside a secure scope.
Warning
There is a common misconception that because a value was produced inside a masked Scope, it will remain masked wherever it's used downstream. This is false. Masking is per-action, not per-value. A value that originated in a secure scope will appear in plain text in any downstream action that doesn't have its own Secure Inputs enabled and that includes that value in its logged inputs.
This containment pattern is especially important when working with orchestrating child flows — you can designate a child flow as the "secure boundary" for all credential operations, ensuring that token retrieval and secret handling are encapsulated in a single flow with appropriately masked run history, while parent flows never directly touch the sensitive values.
The second pattern is about reducing what needs to be masked in the first place. Design your flows so that sensitive data travels through as few actions as possible. Every action that touches sensitive data is an action you need to mask.
For example, instead of:
[HTTP: Get Token] → [Compose: Format Token] → [Set Variable: Store Token] → [HTTP: Call API with Token]
Prefer:
[HTTP: Get Token (Secure In/Out)] → [HTTP: Call API (Secure Inputs, token referenced directly from previous action output)]
The Compose action in the first example is an unnecessary waypoint that would require its own masking. By referencing the previous action's output directly (using @outputs('Get_OAuth_Token')?['body']?['access_token']), you eliminate the intermediate step, reduce the number of actions that need masking, and simplify the flow.
Flow variables in Power Automate are not inherently secure. But they also don't create run history entries of their own — you only see variable values when they appear in the inputs or outputs of actions that use them. This creates a discipline opportunity.
For any variable that will hold a sensitive value:
varBearerToken, varCustomerSSN, varApiKey.One useful technique: do a text search of your flow definition JSON for the variable name. Every @variables('varBearerToken') reference in the JSON tells you exactly which action uses that variable. Check every one of those actions for the presence of secureData properties.
Any action that retrieves data from an external system and returns a response body that could contain PII or sensitive fields should have Secure Outputs enabled, even if you're not certain the response will contain sensitive data in practice. Here's why: you don't control what those external APIs return. An API that today returns only an employee's name and department could be updated to include salary and personal contact information in the response body. If you haven't enabled Secure Outputs, that new PII immediately begins flowing into run history.
Adopt a posture of "secure by default" for all external data retrieval. The cost of debugging is slightly higher (you can't see outputs in run history), but the risk of accidentally logging PII is eliminated.
This posture aligns with the broader securing Power Automate flows in production guidance around connection references and DLP — defense in depth means assuming that any external system could return sensitive data at any time.
Run history masking and secrets management are two halves of the same security requirement. Masking prevents secrets from being logged once they're in the flow. Key Vault integration prevents secrets from being stored in the flow definition itself. You need both.
When a flow retrieves a secret from Azure Key Vault using the Key Vault connector, the action that calls Key Vault will log its output — which includes the secret value — in run history unless you explicitly enable Secure Outputs on that action. This is the most common masking gap we see in organizations that have otherwise done everything right with Key Vault.
The correct configuration for a Key Vault secret retrieval action:
@outputs('Get_Secret')?['body']?['value']), enable Secure Inputs.Key insight
The Azure Key Vault connector's Get Secret action is one of the few connectors where Microsoft's own documentation explicitly recommends enabling Secure Outputs. But the Power Automate designer doesn't automatically enforce this — you must set it manually. If you use the Key Vault connector without enabling Secure Outputs, you've replaced the problem of storing secrets in environment variables with the problem of logging them in run history every time they're retrieved.
For production environments, the full secure architecture for secret handling looks like this:
The Azure Key Vault and Managed Identities integration guide covers the connection configuration and managed identity setup in detail — make sure you're reading that alongside this article if you haven't already.
Individual flow security is important, but enterprise governance requires you to verify masking posture across hundreds or thousands of flows. Manual review doesn't scale. You need automated detection.
The Center of Excellence Toolkit (CoE Starter Kit) maintains an inventory of all flows in your tenant, including their definitions. Once the CoE sync flows have populated Dataverse with your flow inventory, you can write a query against the admin_flow entity to retrieve flow definitions and parse them for masking gaps.
A Power Automate flow that performs this audit would:
runtimeConfiguration.secureData.properties includes "inputs" and/or "outputs" as appropriate.The logic for checking an action's secure configuration in Power Automate expressions would look something like this in a Parse JSON output traversal:
@not(
and(
contains(
string(item()?['runtimeConfiguration']?['secureData']?['properties']),
'outputs'
),
equals(item()?['type'], 'Http')
)
)
This returns true for any HTTP-type action that lacks Secure Outputs — a candidate for remediation. You'd combine this with notification logic to alert flow owners.
For a complete audit and governance framework, see Auditing and Governing Power Automate at Scale — the CoE Toolkit patterns described there are directly applicable to masking compliance.
If your organization deploys flows through ALM pipelines (which you absolutely should be doing in a production environment), you can add a linting step to your deployment pipeline that validates masking configuration before any solution is promoted to production.
Here's a PowerShell script snippet that performs this validation on a solution package:
param(
[string]$SolutionZipPath
)
# Extract the solution zip
$extractPath = ".\solution_extracted"
Expand-Archive -Path $SolutionZipPath -DestinationPath $extractPath -Force
# Find all flow definition files
$flowDefinitions = Get-ChildItem -Path $extractPath -Filter "definition.json" -Recurse
$violations = @()
foreach ($file in $flowDefinitions) {
$definition = Get-Content $file.FullName | ConvertFrom-Json
$actions = $definition.actions
foreach ($actionName in $actions.PSObject.Properties.Name) {
$action = $actions.$actionName
$actionType = $action.type
# Check HTTP actions for secure outputs
if ($actionType -eq "Http") {
$secureProps = $action.runtimeConfiguration?.secureData?.properties
if (-not ($secureProps -contains "inputs")) {
$violations += [PSCustomObject]@{
Flow = $file.Directory.Name
Action = $actionName
Issue = "HTTP action missing Secure Inputs"
}
}
if (-not ($secureProps -contains "outputs")) {
$violations += [PSCustomObject]@{
Flow = $file.Directory.Name
Action = $actionName
Issue = "HTTP action missing Secure Outputs"
}
}
}
}
}
if ($violations.Count -gt 0) {
Write-Host "##vso[task.logissue type=error]Masking violations found:"
$violations | Format-Table -AutoSize
exit 1
}
Write-Host "All HTTP actions have required secure configuration."
This script will cause your pipeline to fail if any HTTP action in the solution lacks the required masking configuration, preventing insecure flows from reaching production. Adapt the action type filtering to include any connector types relevant to your organization (Key Vault, custom connectors, database connectors that might return PII).
Tip
Integrate this linting step as a pull request gate in your Azure DevOps CI/CD pipeline. That way, masking violations are caught during code review before the developer has even submitted for deployment, rather than at deployment time. The earlier you catch it in the lifecycle, the cheaper it is to fix.
The Power Platform Admin API allows you to retrieve run history programmatically. For a sampling-based audit approach, you can periodically retrieve run history records for sensitive flows and check whether inputs and outputs are properly masked by verifying that the logged values match the sentinel placeholder rather than actual data.
This is useful as a second layer of validation — confirming that masking is working in production runs, not just that the configuration exists in the flow definition.
GET https://api.flow.microsoft.com/providers/Microsoft.ProcessSimple/environments/{environmentId}/flows/{flowId}/runs?api-version=2016-11-01
For each run record returned, the inputs and outputs for masked actions will appear as:
{
"inputs": {},
"inputsLink": {
"contentVersion": "...",
"contentSize": 0,
"contentHash": {...},
"secureData": {}
}
}
The presence of secureData: {} in the inputsLink object confirms masking is active. If instead you see a contentLink with a non-zero contentSize, the content is stored and accessible — which means masking is not applied to that action in that run.
Run history masking isn't just a security best practice — in regulated industries, it's a compliance requirement. Understanding the specific regulatory drivers helps you prioritize which flows and which data types to harden first.
Under GDPR, personal data must be processed lawfully and stored only as long as necessary for the stated purpose. Run history that contains PII — names, email addresses, national identification numbers, financial data — represents storage of that PII in Microsoft's infrastructure, subject to the same GDPR obligations as your primary data stores. If your stated retention requirement for a particular category of PII is 30 days, but your flow run history is retaining it for 28 days in a separate system that you've never documented in your data processing inventory, you have a compliance gap.
Secure Outputs masking eliminates this issue by ensuring PII is never written to run history in the first place. This is far cleaner than attempting to manage run history retention as a separate data processing activity.
For flows that process Protected Health Information (PHI), the HIPAA Security Rule's requirements around access controls and audit logging are particularly relevant. Run history represents an audit log of flow execution — and if that audit log contains PHI, it must be subject to the same access controls as your other PHI systems. That means run history access must be restricted to authorized users, and access to that run history must itself be audited.
In practice, most Power Automate environments don't implement the granular access controls on run history that HIPAA would require. The pragmatic solution is to ensure that PHI never enters run history by enabling Secure Outputs on any action that processes PHI responses, and enabling Secure Inputs on any action that sends PHI in its request body.
SOC 2 auditors will ask about access controls on systems that process or store sensitive data. Run history in Power Automate is a system that can store sensitive data — unless you've explicitly configured masking. For SOC 2 Type II, you need to demonstrate that:
The CI/CD gate approach gives you artifact-level evidence of policy enforcement for every deployment throughout the audit period — exactly what SOC 2 auditors want to see.
Let's build a production-ready pattern for a flow that retrieves an API key from Azure Key Vault, calls an external HR API to retrieve employee information including salary data, and processes the response — all without exposing any sensitive data in run history.
You're building a flow triggered by a SharePoint list item update (an employee record flagged for quarterly compensation review). The flow needs to retrieve the HR API key from Key Vault, call the HR API to get the employee's full compensation profile, and write a summary to a Dataverse table for the compensation review board. The employee's SSN and bank account details must never appear in run history.
The SharePoint trigger itself only receives metadata about which list item changed — no PII in the trigger payload in this case. Trigger Secure Inputs is not required, but you should confirm this by reviewing what SharePoint includes in the trigger output schema. For this exercise, we'll leave the trigger unmasked.
Add an Azure Key Vault - Get Secret action named Get HR API Key.
In action settings:
Add a Scope action named [SECURE] HR API Call. Inside this scope:
3a. Add an HTTP action named Get Employee Compensation Profile:
https://hrapi.internal.example.com/v2/compensation/@{triggerOutputs()?['body/employeeId']}Authorization: Bearer @{outputs('Get_HR_API_Key')?['body']?['value']}3b. Add a Parse JSON action named Parse Compensation Response:
@{outputs('Get_Employee_Compensation_Profile')?['body']}3c. Add a Set Variable action for the fields you need downstream (salary and department only — deliberately exclude SSN and bank account from variables, since you don't need them for the compensation review summary).
Outside the Secure Scope, add a Dataverse - Add a new row action:
This action's inputs contain only non-sensitive data — no API key, no SSN. Secure Inputs is not required here, but enabling it anyway is a conservative choice if salary figures are considered sensitive in your organization.
Save and run the flow with a test trigger. Navigate to run history and open the run. Verify:
Note
If you need to debug a failure inside a masked Scope or action, you'll need to temporarily disable masking in a non-production environment. Never disable masking in production to debug — instead, reproduce the issue in a development environment, debug there, and re-enable masking before promoting the fix. This is another argument for maintaining strong environment separation; see Environment Strategy for Power Platform for the broader context.
Enabling Secure Inputs and Secure Outputs on a Scope action masks the Scope's own inputs and outputs — but the individual actions inside the Scope still have their own run history entries, which are logged separately. Many developers enable masking on the Scope and assume the contents are protected. Check each action inside the Scope individually.
Fix: Select each action inside the Scope, open Settings, and verify the secure toggles are configured correctly.
When a sensitive value is first stored in a variable using Initialize Variable or Set Variable, that action logs the variable name and its initial value in its inputs. If you initialize varApiKey to a Key Vault output without masking, the API key appears in run history at the Initialize Variable step.
Fix: Enable Secure Inputs on any Initialize Variable or Set Variable action that stores a sensitive value.
The Compose action is extremely useful for debugging — you can pass any expression into it and inspect the result in run history. This makes it a common masking bypass: developers add a Compose action to check whether they extracted the right field from a Key Vault response, forget to remove it, and promote the flow with a plaintext dump of the secret sitting in a Compose action's output.
Fix: In your deployment checklist and CI/CD pipeline linting, explicitly flag any Compose action whose input expression references variables or outputs from actions that handle sensitive data.
By default, run-only users (users who have been granted run-only access to a flow) cannot see run history. But if run-only access has been granted through a SharePoint list or similar mechanism that also grants broader access, or if the user has been granted Owner access "just for convenience," they will have full run history visibility. Don't assume sharing configuration protects you from run history exposure; masking is the right control.
When an action fails, Power Automate can be configured to continue on failure and execute error handling logic. If your error handler includes an action that logs the error details — say, sending an email with the action's error message or logging it to Application Insights — be aware that the error message may contain fragments of the input data that caused the failure.
For example, if an HTTP call with a malformed bearer token fails, the error response might include the token value in the error body. If your error handler logs @{outputs('Get_Employee_Compensation_Profile')['error']['message']} to a logging system, that error message may contain the token.
Fix: Enable Secure Inputs on all actions in your error handling branches that log error details from previously masked actions. Alternatively, extract only the error code (a non-sensitive field) rather than the full error message.
Warning
This mistake is particularly dangerous because error paths are often tested less thoroughly than happy paths. Build your error handling branches with the same masking discipline as your main flow logic, and include error path scenarios in your security testing.
If your flow is triggered by an HTTP request (a webhook or an HTTP trigger from another system) and the triggering request includes sensitive data in its body or headers, that data is logged in the trigger's run history entry unless you enable Secure Inputs on the trigger.
This is extremely common with flows that accept webhooks from external systems. The calling system sends an event payload that includes customer data, and the entire payload sits in run history.
Fix: For any HTTP-triggered flow that accepts payloads containing PII, credentials, or API keys, enable Secure Inputs on the trigger itself through the trigger settings panel.
Run history exposure is one of the most quietly pervasive security gaps in Power Automate production environments because it doesn't feel like a vulnerability — it feels like a debugging feature. But the data sitting in your flow run history has the same value to an attacker (or an auditor) as the data in your production database. Bearer tokens can be replayed. API keys grant API access. PII represents genuine regulatory liability.
Here's what you've mastered in this lesson:
Where to go next:
The natural progression from run history masking is understanding the full observability story — what do you log instead of raw action inputs and outputs, so that you can still debug production issues without sensitive data exposure? Structured telemetry with correlation IDs, written to Application Insights with deliberately sanitized fields, is the answer. See Implementing Correlation IDs and End-to-End Distributed Tracing and Building a Power Automate Monitoring and Alerting System for the practical patterns.
You should also explore how DLP policies at the connector level complement masking at the action level — they're different controls that work together. Configuring Data Loss Prevention Policies for Enterprise Cloud Flows covers the connector classification framework that determines which connectors can be used together, adding another layer of defense against unintended data exposure.