Custom connector policies let you enforce URL routing, token injection, and throttle-signal normalization across every Power Automate flow that touches a given API — without touching individual flow definitions. This lesson teaches you how to build production-grade governance at the connector layer using RouteRequest, SetHeader, and SetProperty policies with realistic enterprise patterns.

Picture this: your organization has finally gotten serious about API governance. You have a fleet of internal and third-party APIs, each with its own authentication scheme, rate limits, and URL conventions. Your Power Automate flows work fine in development — but the moment you push them toward production volumes, the cracks appear. One API throttles you with no useful retry guidance in the response. Another expects a tenant-specific subdomain that changes between environments. A third uses a legacy auth token format that your flows have been embedding directly in action definitions, which means secrets are showing up in exported solution files.
All three of these problems are solvable at the custom connector layer — before requests even leave Power Automate — using connector policies, runtime request transformation, and structured token injection. This is the difference between an integration that works in a demo and one that runs reliably in a production tenant with dozens of flows hitting the same APIs.
By the end of this lesson, you'll understand exactly how custom connector policies are structured, how to write policy templates that rewrite URLs, inject headers, and handle throttle signals from upstream APIs. You'll be able to build a production-ready custom connector that abstracts away environment-specific configuration and authentication complexity, leaving your flow authors with a clean, safe surface to work against.
What you'll learn:
Retry-After, X-RateLimit-*) in policy logicYou should already be comfortable with custom connectors at a foundational level — if you need to build that foundation first, work through Advanced Power Automate: Custom Connectors and HTTP Actions for Production Integration before continuing. You should also understand how environment variables work in solutions, since we'll use them heavily here. If your secrets management picture isn't clear yet, Integrating Power Automate with Azure Key Vault and Managed Identities is the companion piece.
Before you write a single policy, you need a clear mental model of where policy templates execute in the request lifecycle. When a flow action calls your custom connector, the request passes through this sequence:
This matters enormously. Policies can modify both the outbound request and the inbound response. They run in the connector runtime, not in your flow. This means a policy can inject a secret that exists as an environment variable on the connector without that secret ever appearing in the flow's action inputs or the flow's exported JSON. That's the governance win.
Custom connector policies are defined in the connector's OpenAPI definition under the x-ms-connector-metadata extension, and individual operation policies live in the x-ms-policies extension on each operation or at the top level of the definition. The policy runtime interprets these as a sequence of transformations applied in declaration order.
Key insight
Policies are declarative, not imperative. You're describing what should happen to the request, not writing procedural code. This means the set of available transformations is finite — but the set covers the vast majority of enterprise integration patterns. When you need true arbitrary logic, that's when you reach for Azure Functions as a compute layer.
Policy types available in Power Automate custom connectors as of the current platform:
| Policy Template | Purpose |
|---|---|
SetHeader |
Add or overwrite an HTTP header on request or response |
SetQueryParameter |
Add or overwrite a query string parameter |
RouteRequest |
Rewrite the request URL (host, path, scheme) |
SetProperty |
Set a property in the request/response body |
ConvertBodyFormat |
Transform between XML, JSON, and form-encoded |
SwitchHost |
Switch base URL based on a parameter value |
Let's build each of the three main governance patterns in turn.
The problem: your internal API lives at https://api-dev.contoso.internal in development, https://api-uat.contoso.internal in UAT, and https://api.contoso.com in production. Each environment uses the same connector definition — because you're deploying it via solution packages — but the base URL needs to be environment-specific without asking every flow author to parameterize it themselves.
First, define an environment variable in your solution for the API base URL. In your solution, create a new environment variable:
contoso_ApiBaseUrlhttps://api-dev.contoso.internalIn each environment (UAT, production), you'll override this value during deployment. If you have an ALM pipeline set up, this maps directly to your pipeline's deployment parameters. For the full strategy around environment variables across deployments, see Deploying and Managing Power Automate Solutions Across Environments.
In the connector's OpenAPI definition (the swagger JSON/YAML you edit in the custom connector designer's "Definition" tab or upload directly), add the following under x-ms-policies at the connector root level:
{
"x-ms-policies": [
{
"description": "Route requests to the environment-specific API base URL",
"type": "RouteRequest",
"conditions": [],
"parameters": {
"newPath": "@connectionParameters('apiBaseUrl'){request.pathsuffix}",
"httpMethod": "@request.httpMethod"
}
}
]
}
And the corresponding connection parameter that surfaces in the connector's connection definition:
{
"connectionParameters": {
"apiBaseUrl": {
"type": "string",
"uiDefinition": {
"displayName": "API Base URL",
"description": "The base URL for the Contoso API (e.g., https://api.contoso.com)",
"tooltip": "Do not include a trailing slash",
"constraints": {
"required": "true"
}
}
}
}
}
When an admin creates the connection (not a flow author — an admin, once, per environment), they paste in the appropriate base URL from the environment variable. All flows that use that connection automatically route to the correct host.
Tip
Combine this with connection references in your solutions. A connection reference abstracts the specific connection from the flow definition, so promoting a solution between environments means updating the connection reference to point to the pre-created production connection — not editing every flow. This is one of the biggest operational wins in enterprise ALM.
The {request.pathsuffix} token in the policy expression captures everything after the base URL in the original request and appends it to the new host. So if your connector's swagger defines a base URL of https://placeholder.contoso.com and an operation path of /v1/orders/{orderId}, the policy will route the assembled path /v1/orders/ORD-12345 to https://api.contoso.com/v1/orders/ORD-12345 transparently.
If your API uses region-specific endpoints — common in globally distributed enterprise systems — you can extend this pattern with SwitchHost:
{
"x-ms-policies": [
{
"description": "Route to region-specific endpoint based on region parameter",
"type": "SwitchHost",
"conditions": [],
"parameters": {
"x-ms-apimTemplateParameter.name": "region",
"x-ms-apimTemplateParameter.in": "query",
"x-ms-apimTemplateParameter.urlEncoding": "none",
"hostTemplates": {
"us-east": "https://api-us-east.contoso.com",
"eu-west": "https://api-eu-west.contoso.com",
"ap-south": "https://api-ap-south.contoso.com"
}
}
}
]
}
When the flow action includes a region query parameter with value eu-west, the connector silently rewrites the host. The region parameter never actually reaches the upstream API if you mark it as an internal connector parameter — you strip it back off in a subsequent SetQueryParameter policy with an empty value.
The governance anti-pattern that shows up repeatedly in production Power Automate estates: API keys and bearer tokens embedded directly in flow action configurations. They're visible in the flow designer, they serialize into solution exports, and they create a nightmare when a key rotates — you have to find and update every flow that referenced it directly.
Token injection at the connector level solves this cleanly. The token lives in the connection (ideally pulled from Azure Key Vault at connection-creation time), and the policy injects it into every outbound request automatically.
For APIs that use a static API key in a custom header — common with SaaS platforms like Salesforce Marketing Cloud, various ERP APIs, or internal microservices:
{
"x-ms-policies": [
{
"description": "Inject API key from connection parameter into X-API-Key header",
"type": "SetHeader",
"conditions": [],
"parameters": {
"name": "X-API-Key",
"value": "@connectionParameters('apiKey')",
"existsAction": "override"
}
}
]
}
The connection parameter definition:
{
"connectionParameters": {
"apiKey": {
"type": "securestring",
"uiDefinition": {
"displayName": "API Key",
"description": "Your Contoso API key. Retrieve from the developer portal.",
"tooltip": "This value is stored encrypted and never exposed in flow definitions.",
"constraints": {
"required": "true",
"clearText": "false"
}
}
}
}
}
The critical detail: type: securestring. This tells the connector runtime to store the value encrypted at rest and to never serialize it in plain text in exported solution packages or flow run history. The value is opaque after entry — the connector runtime can use it in policy execution, but no human (or export) can read it back out.
Warning
type: string connection parameters do appear in exported solution packages and flow run histories. Always use type: securestring for any credential or token, even if the API documentation doesn't emphasize security. The performance difference is zero; the security difference is enormous.
For APIs using OAuth2 Bearer tokens, the connector definition supports an oauthSetting that handles the full token acquisition and refresh lifecycle. But for APIs with non-standard OAuth2 flows — or APIs that use token introspection — you sometimes need to inject a manually managed bearer token:
{
"connectionParameters": {
"bearerToken": {
"type": "securestring",
"uiDefinition": {
"displayName": "Bearer Token",
"description": "JWT bearer token obtained from your identity provider",
"constraints": {
"required": "true",
"clearText": "false"
}
}
}
},
"x-ms-policies": [
{
"description": "Inject bearer token as Authorization header",
"type": "SetHeader",
"conditions": [],
"parameters": {
"name": "Authorization",
"value": "@concat('Bearer ', connectionParameters('bearerToken'))",
"existsAction": "override"
}
}
]
}
The @concat() expression is evaluated in the policy runtime context, not the Power Automate expression engine — but the syntax is identical to what you'd write in a flow expression, which makes it familiar. The existsAction: override ensures that even if a flow author somehow passed an Authorization header explicitly, the connector replaces it with the governed token. This is your enforcement mechanism.
The most production-ready pattern combines this with Azure Key Vault: the admin creates the connection by pasting in a client secret retrieved from Key Vault. But you can go one step further with a two-part policy that first acquires a token and then injects it — for APIs using the OAuth2 client credentials flow.
This requires a slightly more sophisticated pattern: a pre-request "token acquisition" call embedded in the connector's authentication configuration using oauthSetting:
{
"connectionParameters": {
"token": {
"type": "oauthSetting",
"oAuthSettings": {
"identityProvider": "aad",
"clientId": "{{CLIENT_ID}}",
"scopes": ["https://contoso.com/api/.default"],
"redirectMode": "Direct",
"redirectUrl": "https://global.consent.azure-apim.net/redirect",
"properties": {
"IsFirstParty": "False",
"IsOnbehalfOf": "False"
},
"customParameters": {
"resourceUri": {
"value": "https://contoso.com/api"
}
}
}
}
}
}
With this configuration, the connector runtime handles the full OAuth2 client credentials token lifecycle — acquisition, caching, refresh — and injects the Authorization: Bearer <token> header automatically on every request. Your flows don't need to think about authentication at all.
This is where most teams leave value on the table. They handle throttling in their flows — adding retry logic with Do Until loops, sleeping on 429s — but they do it per-flow, inconsistently, with magic numbers for retry delays. The connector layer is the right place to standardize rate-limit behavior across every flow that touches a given API.
For foundational context on why throttling deserves serious attention at production scale, see Handling Pagination and Throttling When Querying Large Datasets in Power Automate.
Modern APIs typically signal rate limits through response headers. The most common patterns:
Retry-After: 30 — wait 30 seconds before retrying (RFC 7231 standard)Retry-After: Wed, 21 Oct 2025 07:28:00 GMT — wait until this timestampX-RateLimit-Limit: 1000 — your total request allowance in this windowX-RateLimit-Remaining: 47 — requests left in the current windowX-RateLimit-Reset: 1698921600 — Unix timestamp when the window resetsX-RateLimit-Retry-After: 15 — Shopify and similar: explicit retry delay in secondsThe connector policy layer can't implement conditional retry logic by itself — policies don't have if/else branching or loop constructs. What they can do is surface these headers in a normalized way so that your flow's error handling always has a consistent property to inspect, regardless of which API generated the 429.
The pattern: use a SetProperty policy on the response to map vendor-specific throttling headers into a normalized response body property your flows can reliably read:
{
"x-ms-policies": [
{
"description": "Normalize throttle retry delay into response body",
"type": "SetProperty",
"conditions": [
{
"property": "@statusCode",
"value": "429",
"type": "equals"
}
],
"parameters": {
"propertyName": "x-ms-throttle-retry-after",
"value": "@coalesce(response.headers['Retry-After'], response.headers['X-RateLimit-Retry-After'], '60')",
"propertyScope": "Response"
}
}
]
}
Note
The conditions array uses simple equality checks on the status code. When the upstream returns 429, this policy fires and adds a x-ms-throttle-retry-after property to the response body that your flow's error handling can read with @outputs('ActionName')?['body/x-ms-throttle-retry-after']. The coalesce() falls back through vendor-specific headers before defaulting to 60 seconds.
Some APIs require you to identify yourself in the request so they can apply per-client rate limits correctly — if you omit the identification header, they lump you in a default pool with much lower limits. The SetHeader policy handles this:
{
"x-ms-policies": [
{
"description": "Identify Power Automate as caller for rate limit bucketing",
"type": "SetHeader",
"conditions": [],
"parameters": {
"name": "X-Client-Id",
"value": "@connectionParameters('clientIdentifier')",
"existsAction": "skip"
}
},
{
"description": "Set request ID for correlation and throttle tracking",
"type": "SetHeader",
"conditions": [],
"parameters": {
"name": "X-Request-Id",
"value": "@guid()",
"existsAction": "override"
}
}
]
}
The existsAction: skip on X-Client-Id means a flow can override it if legitimately needed, while the X-Request-Id always gets overwritten with a fresh GUID — which is exactly what you want for correlation, since a flow author copying an action shouldn't accidentally replay a previous request ID.
The connector policy normalizes the signals. Your flow's error handling consumes them. Here's the retry scope pattern you'd add to any flow calling this connector:
Scope: Call Contoso API with retry
HTTP Action: POST /v1/orders
Configure run after: on failure, on timeout
Condition: Is throttled?
Left: @outputs('POST_v1_orders')?['statusCode']
Equals: 429
If yes:
Delay action:
Count: @int(outputs('POST_v1_orders')?['body/x-ms-throttle-retry-after'])
Unit: Second
HTTP Action: POST /v1/orders (retry)
Because the connector policy always provides x-ms-throttle-retry-after on a 429, the int() conversion will always succeed — no null checks, no fallback expressions needed in the flow. The normalization happened upstream.
Key insight
This is the governance value proposition in miniature. The connector defines the contract. Every flow that uses this connector gets consistent throttle-handling behavior. When the upstream API changes its throttling headers — and they will — you update one connector policy, and all flows benefit automatically.
In practice, these patterns don't live in isolation. A production-grade enterprise connector typically combines all of them into a coherent governance envelope. Here's an annotated full policy block for a realistic internal ERP API:
{
"swagger": "2.0",
"info": {
"title": "Contoso ERP API",
"description": "Enterprise connector for Contoso ERP with policy-based governance",
"version": "2.1.0"
},
"host": "placeholder.contoso.com",
"basePath": "/",
"schemes": ["https"],
"connectionParameters": {
"apiBaseUrl": {
"type": "string",
"uiDefinition": {
"displayName": "ERP API Base URL",
"description": "Environment-specific base URL (e.g., https://erp.contoso.com)",
"constraints": { "required": "true" }
}
},
"clientId": {
"type": "string",
"uiDefinition": {
"displayName": "Client ID",
"description": "OAuth2 client identifier for this Power Automate environment",
"constraints": { "required": "true" }
}
},
"apiKey": {
"type": "securestring",
"uiDefinition": {
"displayName": "API Key",
"description": "API key from the ERP admin console",
"constraints": { "required": "true", "clearText": "false" }
}
}
},
"x-ms-policies": [
{
"description": "1. Route to environment-specific host",
"type": "RouteRequest",
"conditions": [],
"parameters": {
"newPath": "@concat(connectionParameters('apiBaseUrl'), request.pathsuffix)",
"httpMethod": "@request.httpMethod"
}
},
{
"description": "2. Inject API key",
"type": "SetHeader",
"conditions": [],
"parameters": {
"name": "X-ERP-API-Key",
"value": "@connectionParameters('apiKey')",
"existsAction": "override"
}
},
{
"description": "3. Set client identity for rate-limit bucketing",
"type": "SetHeader",
"conditions": [],
"parameters": {
"name": "X-Client-Id",
"value": "@connectionParameters('clientId')",
"existsAction": "skip"
}
},
{
"description": "4. Set correlation ID on every request",
"type": "SetHeader",
"conditions": [],
"parameters": {
"name": "X-Correlation-Id",
"value": "@guid()",
"existsAction": "override"
}
},
{
"description": "5. Normalize throttle signal on 429 responses",
"type": "SetProperty",
"conditions": [
{
"property": "@statusCode",
"value": "429",
"type": "equals"
}
],
"parameters": {
"propertyName": "retryAfterSeconds",
"value": "@coalesce(response.headers['Retry-After'], response.headers['X-RateLimit-Retry-After'], '30')",
"propertyScope": "Response"
}
}
]
}
Policies execute in declaration order. The RouteRequest fires first (policy 1), sending the request to the correct environment host. The SetHeader policies (2–4) fire next, injecting credentials and correlation identifiers. The SetProperty policy (5) fires on the response if the upstream returned 429, normalizing the retry signal.
Tip
Keep your policy block well-commented with the numbered description fields. When you're debugging a connector six months later — or onboarding a new platform engineer — the description fields appear in the connector definition UI and serve as inline documentation. Treat them like code comments, not formalities.
The policy expression engine is strict. If connectionParameters('apiKey') is empty because an admin forgot to fill it in during connection creation, the SetHeader policy will inject an empty X-ERP-API-Key header — which the upstream API will reject with a 401. You won't get a friendly error; you'll get a mysterious authentication failure.
Mitigate this by making all credential connection parameters explicitly required (the "required": "true" constraint in uiDefinition). The connection creation UI will then refuse to proceed without the value.
URL path segments that contain special characters (slashes, plus signs, spaces) can break the {request.pathsuffix} token if the connector runtime URL-encodes them differently than your API expects. Test this explicitly with path parameters containing special characters early in development, not after you've built twenty operations on top of the connector.
When an admin saves a new connection, the connector runtime fires a validation request to the connector's x-ms-testOperation endpoint. Policies do fire for this call — but connection parameters may not be fully populated yet if the validation happens before the admin finishes filling the form. If your validation endpoint requires the API key, ensure your test operation returns a useful error message when the key is absent rather than a generic 500.
You can stack multiple SetHeader policies — the runtime applies them in order. This is intentional and useful. You might have one policy that always sets X-ERP-API-Key and a second that conditionally sets X-ERP-Debug-Mode only for non-production connections (though the latter requires the conditions array, which currently supports only status-code and header-presence checks, not arbitrary expression evaluation).
A beautifully designed connector policy layer is undermined if flow authors can bypass it by creating their own generic HTTP actions to the same API. This is where DLP policy enforcement and connector classification become essential governance tools. For a deep dive into that enforcement layer, see Configuring Data Loss Prevention Policies for Enterprise Cloud Flows.
The operational principle: classify your enterprise custom connectors as Business data connectors, restrict them to specific environments (not tenant-wide), and use Managed Environments' connector sharing controls to ensure only approved connections (created by admins with the correct credentials) can be used in flows. If you're using the CoE Toolkit, you can audit which flows have bypassed your connectors in favor of raw HTTP — which is a valuable governance signal. The toolkit setup is covered in Auditing and Governing Power Automate at Scale.
In this exercise, you'll build a custom connector for a public-facing paginated API (the GitHub REST API works well — it has explicit rate-limit headers and realistic pagination) with full policy governance applied.
Step 1: Create the base connector
In Power Automate, navigate to Data > Custom connectors > New custom connector > Create from blank. Name it "GitHub API (Governed)". Set the scheme to HTTPS, the host to api.github.com, and the base URL to /.
Step 2: Add connection parameters
Switch to the "Definition" tab and upload a swagger definition that includes these connection parameters:
"connectionParameters": {
"personalAccessToken": {
"type": "securestring",
"uiDefinition": {
"displayName": "Personal Access Token",
"description": "GitHub PAT with appropriate scopes",
"constraints": { "required": "true", "clearText": "false" }
}
},
"clientIdentifier": {
"type": "string",
"uiDefinition": {
"displayName": "Client Identifier",
"description": "Unique identifier for this Power Automate environment (e.g., contoso-prod)",
"constraints": { "required": "true" }
}
}
}
Step 3: Define one operation
Add a GET operation for /repos/{owner}/{repo}/issues with path parameters owner and repo, and query parameters state (string, enum: open/closed/all) and per_page (integer, default 30, max 100).
Step 4: Apply the policy block
Add these policies to the swagger's root-level x-ms-policies:
"x-ms-policies": [
{
"description": "Inject GitHub PAT as Bearer token",
"type": "SetHeader",
"conditions": [],
"parameters": {
"name": "Authorization",
"value": "@concat('token ', connectionParameters('personalAccessToken'))",
"existsAction": "override"
}
},
{
"description": "Set User-Agent (required by GitHub API)",
"type": "SetHeader",
"conditions": [],
"parameters": {
"name": "User-Agent",
"value": "@concat('PowerAutomate-', connectionParameters('clientIdentifier'))",
"existsAction": "override"
}
},
{
"description": "Normalize rate limit on 403 (GitHub uses 403 for secondary rate limits)",
"type": "SetProperty",
"conditions": [
{ "property": "@statusCode", "value": "403", "type": "equals" }
],
"parameters": {
"propertyName": "rateLimitRetryAfter",
"value": "@coalesce(response.headers['Retry-After'], response.headers['X-RateLimit-Reset'], '60')",
"propertyScope": "Response"
}
}
]
Note that GitHub uses token <PAT> format, not Bearer — the policy handles that prefix correctly. The User-Agent header is mandatory for GitHub's API; without it, you get 403s. This is exactly the kind of API-specific requirement that belongs in the connector policy, not scattered across every flow.
Step 5: Create a flow that uses it
Build a simple cloud flow that calls your connector's "List Issues" operation and logs the X-RateLimit-Remaining response header to a SharePoint list. Add error handling that checks for a 403 status code and reads body/rateLimitRetryAfter to set a delay before retrying.
This gives you a complete, self-contained governance example with real API behavior to observe.
"My policy doesn't seem to be firing"
First check: did you update the connector definition after adding the policy block? Policies in an edited swagger aren't live until you save and update the connector. If you're updating a connector that's already in use, existing connections pick up the new policies immediately — but test with a fresh flow run, not a replay of an old run.
"The RouteRequest policy is rewriting to the wrong path"
Use the @request.pathsuffix token (not @request.path) to capture the path component without the base URL. If you're also sending query parameters, confirm they're not being dropped — the route rewrite preserves query string parameters by default unless you explicitly overwrite them with SetQueryParameter.
"Securestring parameters are showing up as empty in my injected header"
This typically means the connection was created before you added the parameter to the connection parameter definition. Delete the connection and recreate it — the connector runtime can't retroactively populate secure parameters into existing connections.
"I can't test policies in the connector designer's test tab"
The test tab in the connector designer doesn't fully simulate policy execution in all cases — particularly for RouteRequest policies, the test might still hit the placeholder host. Always validate policy behavior with a real cloud flow calling the connector, not just the designer's test panel.
"My conditional policy fires on every response, not just 429s"
Double-check your conditions array syntax. The property value for status code conditions must be a string ("429"), not an integer. A malformed condition silently matches everything — the runtime doesn't throw an error for a bad condition expression.
Warning
When you export a solution containing a custom connector, the swagger definition — including all policy declarations — travels with it. Connection parameter values (especially securestrings) do not travel with the export. This is by design. Your ALM process must include a step where an admin creates connections with the correct credentials in each target environment before deploying the solution. Document this dependency explicitly in your runbook.
Custom connector policies give you a clean enforcement layer between your flows and the APIs they consume. You've seen how URL rewriting handles environment promotion without touching flow definitions, how token injection keeps secrets out of flow configurations entirely, and how response transformation normalizes throttling signals into consistent structures your error-handling logic can depend on.
The key architectural principle running through all three patterns: decide once, enforce everywhere. When the governance logic lives in the connector, every flow that uses that connector gets it automatically. New flows inherit it. Updated policies propagate immediately. This is the difference between a governance strategy that degrades over time as the flow estate grows and one that scales with it.
Your natural next moves from here:
If you're using Azure API Management as the central API gateway, explore how to layer connector policies with APIM's own transformation and throttling policies — the two complement each other, with APIM handling cross-system concerns and connector policies handling Power Automate-specific adaptation. See Fronting Power Automate HTTP Endpoints with Azure API Management for the integration pattern.
To make your throttle-handling flows more robust under high concurrency — especially when multiple parallel branches are all hitting the same rate-limited API — combine these connector patterns with the concurrency controls described in Implementing Parallel Branching and Concurrency Control in Power Automate.
For high-volume scenarios where you're hitting the API's absolute rate ceiling regardless of retry logic, consider buffering requests through Azure Service Bus so the downstream API sees a metered, controlled call rate. That architecture is covered in Implementing Event-Driven Automation with Power Automate and Azure Service Bus.
The connector is your API governance boundary. Build it like you mean it.