Environment variables are the key to building Power Platform solutions that deploy cleanly across dev, UAT, and production without manual edits. This deep-dive lesson covers the full lifecycle: creating typed variables, managing the default/current value split, binding connection references, and injecting overrides at deployment time using Power Platform CLI and deployment settings files.

Picture this: your team has spent three weeks building a sophisticated model-driven app for a field services company. The app pulls configuration data from a SharePoint list, calls an external REST API for weather routing, and sends notifications through a shared inbox. Everything works perfectly in the development environment. Then you promote the solution to UAT, and the entire thing points back at the development SharePoint site, the dev team's email inbox, and a sandbox API endpoint that doesn't exist in the UAT tenant. You're back to manually hunting through flows and connections, editing hardcoded values one by one — hoping you haven't missed anything.
Environment variables exist precisely to prevent that scenario. They give you a structured, solution-aware way to externalize configuration — API endpoints, SharePoint site URLs, feature flags, numeric thresholds, connection references — so that when you move a solution from dev to UAT to production, deployment-time values slot in cleanly without touching your app logic. This isn't just convenient; it's the difference between a maintainable ALM (Application Lifecycle Management) pipeline and a configuration nightmare that only the original developer can navigate.
By the end of this lesson, you'll know how to design, configure, and deploy environment variables across the full Power Platform ALM lifecycle. You'll understand the difference between default values and current values, how connection references relate to environment variables, and how to automate override injection at deployment time using deployment pipelines and the CLI.
What you'll learn:
This lesson assumes you're comfortable with the Power Platform solutions model — specifically, the difference between managed and unmanaged solutions, publisher prefixes, and solution layering. If you need a refresher, Solutions for Model-Driven Apps: Publishers, Managed vs Unmanaged, and Solution Layering covers those foundations thoroughly.
You should also have a working understanding of Dataverse tables and how model-driven apps consume them. Familiarity with Dataverse Fundamentals: Tables, Columns, and Rows Explained for Power Apps Makers will help you understand where environment variable data lives under the hood.
You'll need:
pac version 1.20 or later)Before you configure anything, you need a mental model of what you're actually working with. Environment variables in Dataverse aren't magical — they're just records in two specific system tables.
environmentvariabledefinition stores the variable itself: its schema name, display name, data type, description, and the default value. This table lives inside your solution. When you export a solution, the definition travels with it.
environmentvariablevalue stores the current value — the actual value that overrides the default in a given environment. This is the key nuance. The current value record is intentionally excluded from managed solution exports by default, because it's meant to be environment-specific.
This separation is deliberate. Your default value says "in dev, this SharePoint site URL is https://contoso.sharepoint.com/sites/FieldServicesDev." Your current value in production says "actually, use https://contoso.sharepoint.com/sites/FieldServicesProd." The definition travels in the solution. The current value either gets set manually after import or gets injected by your deployment pipeline.
Key insight
If you don't set a current value in a target environment, the system falls back to the default value from the definition. This is safe behavior for development but dangerous for production — you need an explicit strategy for ensuring current values exist before the app goes live.
When a Power Automate flow or canvas app reads an environment variable, it reads the current value if it exists, and falls back to the default. If neither exists, the variable returns null/blank — and depending on how your flow is written, that can cause silent failures or runtime errors.
You create environment variable definitions inside a solution — never directly in the environment without a solution context. Here's why: if you create them outside a solution, they end up in the Default Solution, which you can't cleanly manage or transport.
Navigate to make.powerapps.com, select your target development environment, and open your solution (not the Default Solution). From the solution's Objects panel on the left, click New and select More → Environment variable.
The creation dialog asks for:
Field Services SharePoint Site URL.contoso, this becomes contoso_FieldServicesSharePointSiteURL. You can't change this after creation.Click Save. The environment variable definition is now part of your solution.
Text covers most use cases: URLs, email addresses, identifiers, API keys (though for secrets, use Secret instead). Maximum 2000 characters.
Number is a decimal type — useful for thresholds like MaxRetryAttempts, InvoiceApprovalThreshold, or PageSizeDefault.
Boolean is your feature flag type. Use it for EnableBetaFeatures, UseNewApprovalFlow, or MaintenanceModeActive. Values are true or false.
JSON is powerful and underused. It lets you store structured configuration objects — think an array of email recipients, a mapping of region codes to managers, or a complex routing configuration. The value is just a string that your flow or app parses as JSON.
Data source is specifically for connector-based connections, which we'll cover in the Connection References section below.
Secret integrates with Azure Key Vault. The environment variable stores a reference to a Key Vault secret, not the secret value itself. This is the right choice for API keys, passwords, and tokens in production scenarios.
Warning
Don't use Text type for secrets, even if you're tempted by its simplicity. Text values are stored in plaintext in the Dataverse environmentvariablevalue table and are visible to anyone with System Administrator access. Use Secret type backed by Key Vault for anything sensitive in production.
This is where most teams get burned, so let's be precise.
When you export your unmanaged solution and import it as a managed solution in UAT, the environmentvariabledefinition record travels with it — including the default value you set in dev. The default value is what gets deployed.
This is intentional. The default value is your safety net. If something goes wrong with your deployment pipeline and no current value gets injected, the app still runs — just against the default (probably dev-pointing) configuration. That's better than null, though clearly not right for production.
The environmentvariablevalue record is not included in managed solution exports by default. When you import a managed solution, any existing current values in the target environment are preserved. New current values don't come in from the export.
This means:
Note
If you're importing an unmanaged solution (which you should only be doing in your dev environment), the solution can carry current values. This is a source of confusion — be intentional about which solution type you're using in which environment.
After importing your managed solution into UAT, navigate to make.powerapps.com in the UAT environment, open the managed solution, find the environment variable, and edit it. You'll see the Current value field is blank. Enter the UAT-specific value here.
This works fine for small teams with one or two environments. It doesn't scale. For any serious ALM pipeline, you need to inject values programmatically.
The Power Platform CLI (pac) gives you precise control over environment variable values at deployment time. This is how you build repeatable, automated deployments.
# Authenticate with a service principal (recommended for CI/CD)
pac auth create --name UAT \
--environment "https://contoso-uat.crm.dynamics.com" \
--applicationId "00000000-0000-0000-0000-000000000001" \
--clientSecret "your-secret-here" \
--tenant "your-tenant-id"
# Switch to the UAT auth profile
pac auth select --name UAT
# List all environment variable definitions in the target environment
pac env list-variables --environment "https://contoso-uat.crm.dynamics.com"
This gives you the schema names and current values, which you'll need for the next step.
# Set a text environment variable current value
pac env update-variable \
--environment "https://contoso-uat.crm.dynamics.com" \
--name "contoso_FieldServicesSharePointSiteURL" \
--value "https://contoso.sharepoint.com/sites/FieldServicesUAT"
# Set a boolean environment variable
pac env update-variable \
--environment "https://contoso-uat.crm.dynamics.com" \
--name "contoso_EnableBetaFeatures" \
--value "false"
# Set a number environment variable
pac env update-variable \
--environment "https://contoso-uat.crm.dynamics.com" \
--name "contoso_InvoiceApprovalThreshold" \
--value "5000"
For production deployments, hardcoding values in shell scripts is fragile. The better approach is a deployment settings JSON file, which captures all environment-specific overrides in one place.
{
"EnvironmentVariables": [
{
"SchemaName": "contoso_FieldServicesSharePointSiteURL",
"Value": "https://contoso.sharepoint.com/sites/FieldServicesProd"
},
{
"SchemaName": "contoso_InvoiceApprovalThreshold",
"Value": "10000"
},
{
"SchemaName": "contoso_EnableBetaFeatures",
"Value": "false"
},
{
"SchemaName": "contoso_NotificationEmailAddress",
"Value": "fieldservices-notifications@contoso.com"
}
],
"ConnectionReferences": [
{
"LogicalName": "contoso_SharePointFieldServices",
"ConnectionId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline/connections/abc123",
"ConnectorId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline"
}
]
}
Deploy with this file:
pac solution import \
--path "./contoso-fieldservices-1.0.0.zip" \
--environment "https://contoso-prod.crm.dynamics.com" \
--settings-file "./prod-deployment-settings.json"
The --settings-file flag applies all environment variable values and connection reference mappings in a single atomic operation during import. This is the pattern you want for any serious CI/CD pipeline — GitHub Actions, Azure DevOps, or Power Platform Pipelines.
Tip
Generate a starter deployment settings file from your solution using pac solution create-settings --path "./contoso-fieldservices-1.0.0.zip" --settings-file "./deployment-settings-template.json". This outputs a template with all the environment variables and connection references from the solution, with blank values for you to fill in. Commit the template to your repo and maintain environment-specific versions (e.g., uat-settings.json, prod-settings.json) — never commit the prod settings file with real secrets to a public repo.
Connection references deserve their own section because they're a distinct but related concept that trips up almost everyone the first time.
A connection reference is a solution component that acts as a pointer to a connector connection. Instead of a flow or app being directly bound to a specific connection (which would make it environment-specific), it binds to a connection reference, which then maps to the actual connection in each environment.
Think of it like this: your flow says "I need a SharePoint connection." The connection reference says "in dev, the SharePoint connection to use is [Alice's dev connection]. In UAT, it's [the UAT service account connection]." The flow itself doesn't change — only the connection reference mapping changes per environment.
When you add a SharePoint or Dataverse action to a Power Automate flow inside a solution, the maker experience automatically offers to create a connection reference. Give it a meaningful name: Field Services SharePoint Connection. The prefix from your publisher gets applied: contoso_FieldServicesSharePointConnection.
You can also create connection references manually: in your solution, click New → More → Connection Reference, select the connector type, and optionally bind a connection right now (for your dev environment).
The connection reference definition (schema name, connector type, display name) travels in the solution export. The connection binding — the actual connection ID that maps to a specific user's or service account's credentials — does not travel. This is by design: connection credentials are environment-specific.
When you import the solution into UAT, the connection reference arrives without a connection bound. Power Automate flows that depend on it will be suspended (turned off) until you bind a connection.
Manual approach: After import, navigate to the solution in the UAT environment, find the connection reference, click on it, and select an existing connection from the environment's available connections (or create a new one).
CLI / deployment settings approach: This is where the deployment settings JSON file shines. The ConnectionReferences block in that file maps the logical name of each connection reference to a specific connection ID in the target environment.
To find the connection ID for a connection in the target environment:
# List available connections for a specific connector in the target env
pac connection list \
--environment "https://contoso-uat.crm.dynamics.com" \
--connector "shared_sharepointonline"
The output gives you connection IDs in the format /providers/Microsoft.PowerApps/apis/shared_sharepointonline/connections/abc123. Use that in your settings file.
Warning
Connection references are user-owned by default — the connection is owned by whichever user or service principal created it. In production, always use service accounts or service principals for connection references rather than individual user accounts. If Alice leaves the company, her connections stop working, and every flow and app bound to those connection references fails simultaneously. This is a production incident waiting to happen.
For production-grade deployments, create connections using a service principal (Azure AD app registration) rather than a user account. The mechanics depend on the connector — for Dataverse and Office 365, this is well-supported. For SharePoint, you'll need to configure app-only access in SharePoint Admin.
The key point: document which service principal or service account owns each connection reference in each environment, and include that ownership in your solution's runbook.
Text, number, and boolean cover simple cases. JSON environment variables open up sophisticated configuration patterns.
Suppose your field services app needs to route work orders to different managers based on region. You could create separate environment variables for each region, or you could store a routing table as a JSON environment variable.
Default value (stored in the definition):
{
"regions": [
{ "code": "NE", "managerEmail": "ne.manager@contoso.com", "escalationDays": 3 },
{ "code": "SE", "managerEmail": "se.manager@contoso.com", "escalationDays": 2 },
{ "code": "MW", "managerEmail": "mw.manager@contoso.com", "escalationDays": 4 },
{ "code": "W", "managerEmail": "w.manager@contoso.com", "escalationDays": 3 }
]
}
Production current value override:
{
"regions": [
{ "code": "NE", "managerEmail": "ne.ops@contoso.com", "escalationDays": 2 },
{ "code": "SE", "managerEmail": "se.ops@contoso.com", "escalationDays": 1 },
{ "code": "MW", "managerEmail": "mw.ops@contoso.com", "escalationDays": 3 },
{ "code": "W", "managerEmail": "w.ops@contoso.com", "escalationDays": 2 }
]
}
In your Power Automate flow, you'd read this variable and parse it:
// In a flow: Get the environment variable value
// Use the "Get Environment Variable" action or reference via formula
// Then in a subsequent action, parse with:
json(outputs('Get_environment_variable')?['body/value'])
This keeps your routing logic entirely in configuration — no flow edits needed when an organization restructure changes who manages which region.
Tip
Keep JSON environment variable schemas simple and flat where possible. Deeply nested JSON is hard to read, harder to override correctly in deployment settings files, and has no schema validation — a typo in the JSON produces a silent failure. Document the schema in the variable's Description field.
The primary way to consume an environment variable in a flow is through the Environment Variable action, found in the Microsoft Dataverse connector actions. You reference the variable by schema name. The returned value is always a string — even for numeric and boolean types — so you need to parse accordingly:
// For a number variable:
int(outputs('Get_EnvVar_InvoiceApprovalThreshold')?['body/value'])
// For a boolean variable:
if(outputs('Get_EnvVar_EnableBetaFeatures')?['body/value'] == 'true', true, false)
// For a JSON variable:
json(outputs('Get_EnvVar_RegionRoutingTable')?['body/value'])
Alternatively, within a flow that uses Dataverse connectors, you can directly reference environment variables by schema name in dynamic content — the flow engine resolves them at runtime.
Canvas Power Apps can consume environment variables via the LookUp function against the environmentvariablevalue and environmentvariabledefinition virtual tables:
// Get the current value, falling back to the default
LookUp(
'Environment Variable Values',
'Environment Variable Definition'.'Schema Name' = "contoso_FieldServicesSharePointSiteURL",
Value
)
For custom pages embedded in model-driven apps, this pattern lets the custom page adapt to environment-specific configuration without any hard-coded values in the canvas formula logic.
Model-driven forms and views don't directly "read" environment variables in formula columns the way canvas apps do. However, environment variables are commonly used to drive behavior in the Power Automate flows and plug-ins that those forms trigger.
For example, a business rule can't directly reference an environment variable — business rules evaluate on the client and aren't aware of the server-side variable store. But a plug-in registered on the PreOperation event of a table can read environment variables from Dataverse using the IOrganizationService, making them a powerful tool for server-side configuration.
If you're using formula columns — which evaluate server-side using Power Fx — note that as of now, formula columns don't support direct environment variable references either. Keep this boundary in mind as you design.
Let's build something real. You'll create a set of environment variables for a field services model-driven app, configure defaults, and simulate a promotion to a second environment.
You need two environments. Call them FieldServicesDev and FieldServicesUAT. Create a solution called FieldServicesCore with publisher prefix fs in the dev environment.
In FieldServicesCore, create the following environment variables:
| Display Name | Schema Name | Type | Default Value |
|---|---|---|---|
| SharePoint Work Orders Site | fs_SPWorkOrdersSiteURL |
Text | https://contoso.sharepoint.com/sites/FSWorkOrdersDev |
| Invoice Approval Threshold | fs_InvoiceApprovalThreshold |
Number | 2500 |
| Beta Features Enabled | fs_BetaFeaturesEnabled |
Boolean | true |
| Notification Config | fs_NotificationConfig |
JSON | See below |
| SharePoint Connection | fs_SPConnection |
Data source (SharePoint) | — |
For fs_NotificationConfig, use this default value:
{
"primaryEmail": "dev-alerts@contoso.com",
"ccEmails": ["dev-lead@contoso.com"],
"sendSMS": false,
"smsNumbers": []
}
Inside the same solution, create a Power Automate flow triggered by a manual button. Add steps to:
fs_SPWorkOrdersSiteURL and log it to a Teams messagefs_InvoiceApprovalThreshold, parse it as integer, and include in the messagefs_NotificationConfig, parse as JSON, and send a test email to the primaryEmail addressTest this in dev. It should use the default values.
# Export as managed solution
pac solution export \
--path "./FieldServicesCore_1.0.0.zip" \
--name "FieldServicesCore" \
--managed true
# Generate deployment settings template
pac solution create-settings \
--path "./FieldServicesCore_1.0.0.zip" \
--settings-file "./uat-settings-template.json"
Open uat-settings-template.json. You'll see all your environment variables with blank values and the connection reference with a blank connection ID.
Create uat-settings.json:
{
"EnvironmentVariables": [
{
"SchemaName": "fs_SPWorkOrdersSiteURL",
"Value": "https://contoso.sharepoint.com/sites/FSWorkOrdersUAT"
},
{
"SchemaName": "fs_InvoiceApprovalThreshold",
"Value": "5000"
},
{
"SchemaName": "fs_BetaFeaturesEnabled",
"Value": "false"
},
{
"SchemaName": "fs_NotificationConfig",
"Value": "{\"primaryEmail\":\"uat-alerts@contoso.com\",\"ccEmails\":[\"qa-lead@contoso.com\"],\"sendSMS\":false,\"smsNumbers\":[]}"
}
],
"ConnectionReferences": [
{
"LogicalName": "fs_SPConnection",
"ConnectionId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline/connections/YOUR_UAT_CONNECTION_ID",
"ConnectorId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline"
}
]
}
pac solution import \
--path "./FieldServicesCore_1.0.0.zip" \
--environment "https://contoso-uat.crm.dynamics.com" \
--settings-file "./uat-settings.json"
After import, run the test flow in the UAT environment. It should use the UAT email addresses and the $5,000 threshold — not the dev defaults.
In the UAT environment, open the FieldServicesCore managed solution and inspect each environment variable. You should see Current value populated with the UAT-specific values, and Default value still showing the dev values from the solution definition.
When you import a managed solution, the environment variable definition shows up in that managed solution. Some makers try to edit the default value there — but you can't (and shouldn't) edit components of a managed solution. You can only set the current value, which is the override layer. If you need to change the default value, change it in the source (unmanaged) solution in dev, then redeploy.
If your flow uses a connection reference that isn't bound to a connection in the target environment, Power Automate suspends the flow after import. This is silent — there's no import error. The flow just sits there, turned off. Always check flow status after a solution import and verify all connection references are bound.
If you set a current value (not just a default) in your dev environment, and then export the unmanaged solution, that current value can be included in the export. When you import that as a managed solution in UAT, the current value from dev can overwrite any UAT-specific current value you'd previously set.
The fix: don't set current values in your dev environment. Use defaults only in dev. Current values are for target environments.
Warning
This is one of the most common environment variable footguns. Double-check your environment variable definitions in dev before exporting. If you see both a default value and a current value set in dev, clear the current value before exporting.
If an environment variable has no default and no current value in the target environment, it returns null. A flow that does int(null) or concatenates null into a URL will either fail silently (the action uses a blank) or throw a runtime error.
Always add a defensive check in flows that consume environment variables:
// Check that the variable isn't blank before using it
if(empty(variables('EnvVarValue')), 'https://fallback-safe-url.com', variables('EnvVarValue'))
Better yet, include a flow validation step at the start that checks all required environment variables are populated and sends an alert if any are blank.
Already covered in the warning above, but worth repeating: Text-type environment variables are not encrypted at rest in any meaningful way. If you need to store an API key, OAuth client secret, or database password, use the Secret type backed by Azure Key Vault. The setup requires a Key Vault and the appropriate Azure permissions, but it's the only production-appropriate approach for sensitive values.
Symptom: You set a current value in the target environment, but the flow or app still seems to use the old value.
Diagnoses:
If the CLI import fails when you specify a settings file, the most common causes are:
Run pac solution import --verbose to get detailed error output.
Environment variables are one of the highest-leverage investments you can make in your Power Platform ALM practice. A solution built around environment variables from the start is genuinely portable — you can promote it through dev, UAT, and production with surgical precision, injecting exactly the right configuration at each stage without touching app logic.
Here's what you've covered:
pac solution create-settings, populate per environment, apply with --settings-file on import.Where to go from here:
For teams building serious ALM pipelines, the next step is integrating these patterns into Power Platform Pipelines (the native CI/CD tooling in Power Platform) or Azure DevOps with the Power Platform Build Tools extension. Both support deployment settings files natively.
If you're building plug-ins that consume environment variables server-side, understanding how plug-in execution contexts work is essential — explore Configuring Dataverse Plug-in Steps and Business Event Handlers: Triggering Server-Side Logic on Table Operations for the plug-in registration side of that story.
If your solution includes complex security configurations that also need to be environment-aware — specific team assignments, security role configurations — understanding how Dataverse Security: Business Units, Security Roles, and Teams layers with your deployment approach will help you build a complete promotion checklist.
Finally, environment variables are an important part of production hygiene, but they're one piece of a larger puzzle. If your app surfaces sensitive data in forms and views, you'll want to ensure your security model travels correctly too — the lesson on Model-Driven App Security: Configuring Security Roles, Field Permissions, and Team-Based Access for Table Data covers what's solution-transportable and what you need to configure per-environment.
Good configuration management is what separates apps that survive their first handoff from apps that require the original developer to be on call forever. Build it right from the start.
Model-Driven Apps & Dataverse
Configuring Dataverse Connection Roles and Relationship Categories: Modeling Party-to-Party Associations Between Records in Model-Driven Apps
Configuring Dataverse Table Capacity and Storage Partitioning: Managing Large-Table Performance, Elastic Tables, and Time-Series Data Strategies in Model-Driven Apps