
Below is an updated script for creating environments in PPAC. This will ask some basic questions for what you’d like to create and can provision development, sandbox, and production environments for F&O.
<#
Interactive Power Platform environment provisioning scriptGoals:
– Collect inputs to New-AdminPowerAppEnvironment using values retrieved from Power Platform
– Localize choices to the end user’s OS (suggest defaults from OS culture/region)
– Show user-friendly language names
– Make currency selection FAST (no per-row region scans)
– Build TemplateMetadata without the user editing JSONDocs:
– Get-AdminPowerAppEnvironmentLocations: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
– Get-AdminPowerAppCdsDatabaseTemplates: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
– Get-AdminPowerAppCdsDatabaseLanguages: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
– Get-AdminPowerAppCdsDatabaseCurrencies: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
– New-AdminPowerAppEnvironment: https://learn.microsoft.com/en-us/powershell/module/microsoft.powerapps….
#>Set-StrictMode -Version Latest
$ErrorActionPreference = “Stop”# —————————-
# Helpers
# —————————-function Install-RequiredModule {
param([Parameter(Mandatory=$true)][string]$Name)if (-not (Get-Module -ListAvailable -Name $Name)) {
Write-Host “Installing module ‘$Name’…”
Install-Module -Name $Name -Force -Scope CurrentUser
}
Import-Module $Name -Force
}function Read-YesNo {
param(
[Parameter(Mandatory=$true)][string]$Prompt,
[bool]$DefaultYes = $true
)
$suffix = if ($DefaultYes) { “[Y/n]” } else { “[y/N]” }
while ($true) {
$raw = Read-Host “$Prompt $suffix”
if ([string]::IsNullOrWhiteSpace($raw)) { return $DefaultYes }
switch ($raw.Trim().ToLowerInvariant()) {
“y” { return $true }
“yes” { return $true }
“n” { return $false }
“no” { return $false }
default { Write-Host “Please answer Y or N.” }
}
}
}function Get-ValidateSetValues {
param(
[Parameter(Mandatory=$true)][string]$CommandName,
[Parameter(Mandatory=$true)][string]$ParameterName
)$cmd = Get-Command $CommandName -ErrorAction SilentlyContinue
if (-not $cmd) { return @() }$p = $cmd.Parameters[$ParameterName]
if (-not $p) { return @() }foreach ($attr in $p.Attributes) {
if ($attr -is [System.Management.Automation.ValidateSetAttribute]) {
return @($attr.ValidValues)
}
}
return @()
}function Get-PropValue {
param(
[Parameter(Mandatory=$true)]$Object,
[Parameter(Mandatory=$true)][string[]]$Candidates
)
foreach ($c in $Candidates) {
if ($null -ne $Object.PSObject.Properties[$c]) {
$v = $Object.$c
if ($null -ne $v -and “$v”.Length -gt 0) { return $v }
}
}
return $null
}function Select-FromList {
param(
[Parameter(Mandatory=$true)][string]$Title,
[Parameter(Mandatory=$true)][array]$Items,
[Parameter(Mandatory=$true)][scriptblock]$ToLabel,
[int]$DefaultIndex = 0
)if (-not $Items -or $Items.Count -eq 0) {
throw “No items available for selection: $Title”
}Write-Host “”
Write-Host $Title
Write-Host (“-” * $Title.Length)for ($i=0; $i -lt $Items.Count; $i++) {
$label = & $ToLabel $Items[$i]
$marker = if ($i -eq $DefaultIndex) { “*” } else { ” ” }
Write-Host (“{0} [{1}] {2}” -f $marker, $i, $label)
}while ($true) {
$raw = Read-Host (“Choose an index (Enter for default {0})” -f $DefaultIndex)
if ([string]::IsNullOrWhiteSpace($raw)) { return $Items[$DefaultIndex] }$n = 0
if ([int]::TryParse($raw, [ref]$n) -and $n -ge 0 -and $n -lt $Items.Count) {
return $Items[$n]
}Write-Host “Invalid selection. Enter a number between 0 and $($Items.Count – 1).”
}
}function Find-BestLocationDefaultIndex {
param(
[Parameter(Mandatory=$true)][array]$Locations,
[Parameter(Mandatory=$true)][System.Globalization.RegionInfo]$Region
)$iso2 = $Region.TwoLetterISORegionName.ToUpperInvariant()
$regionEnglish = $Region.EnglishName$preferredNameMap = @{
“US” = “unitedstates”
“GB” = “unitedkingdom”
“UK” = “unitedkingdom”
“CA” = “canada”
“AU” = “australia”
“DE” = “germany”
“FR” = “france”
“NL” = “netherlands”
“SE” = “sweden”
“NO” = “norway”
“DK” = “denmark”
“IE” = “ireland”
“JP” = “japan”
“IN” = “india”
“BR” = “brazil”
}$locNameCandidate = $null
if ($preferredNameMap.ContainsKey($iso2)) { $locNameCandidate = $preferredNameMap[$iso2] }for ($i=0; $i -lt $Locations.Count; $i++) {
$ln = Get-PropValue -Object $Locations[$i] -Candidates @(“LocationName”,”locationName”)
if ($locNameCandidate -and $ln -and ($ln.ToString().ToLowerInvariant() -eq $locNameCandidate)) {
return $i
}
}for ($i=0; $i -lt $Locations.Count; $i++) {
$dn = Get-PropValue -Object $Locations[$i] -Candidates @(“LocationDisplayName”,”DisplayName”,”locationDisplayName”,”name”)
if ($dn -and $dn.ToString().IndexOf($regionEnglish, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
return $i
}
}for ($i=0; $i -lt $Locations.Count; $i++) {
$dn = Get-PropValue -Object $Locations[$i] -Candidates @(“LocationDisplayName”,”DisplayName”,”locationDisplayName”,”name”)
if ($dn -and $dn.ToString().IndexOf($iso2, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
return $i
}
}return 0
}function Find-DefaultIndexByPredicate {
param(
[Parameter(Mandatory=$true)][array]$Items,
[Parameter(Mandatory=$true)][scriptblock]$Predicate
)
for ($i=0; $i -lt $Items.Count; $i++) {
if (& $Predicate $Items[$i]) { return $i }
}
return 0
}function Get-FriendlyCultureNameFromLcid {
param([Parameter(Mandatory=$true)][int]$Lcid)
try {
$ci = [System.Globalization.CultureInfo]::GetCultureInfo($Lcid)
return “{0} / {1} — LCID {2}” -f $ci.EnglishName, $ci.NativeName, $Lcid
} catch {
return “LCID $Lcid”
}
}function Get-LcidFromLanguageItem {
param([Parameter(Mandatory=$true)]$Item)if ($Item -is [int]) { return $Item }
$tmp = 0
if ($Item -is [string] -and [int]::TryParse($Item, [ref]$tmp)) { return $tmp }$preferredProps = @(
“LanguageCode”,”languageCode”,
“Lcid”,”LCID”,
“LocaleId”,”localeId”,
“Code”,”code”,
“Value”,”value”,
“Id”,”id”
)foreach ($p in $preferredProps) {
if ($null -ne $Item.PSObject.Properties[$p]) {
$v = $Item.$p
if ($v -is [int]) { return $v }
if ([int]::TryParse(“$v”, [ref]$tmp)) { return $tmp }
}
}foreach ($prop in $Item.PSObject.Properties) {
$v = $prop.Value
if ($v -is [int]) {
if ($v -ge 1 -and $v -le 99999) { return $v }
}
if ([int]::TryParse(“$v”, [ref]$tmp)) {
if ($tmp -ge 1 -and $tmp -le 99999) { return $tmp }
}
}return $null
}function Get-LanguageFriendlyLabel {
param([Parameter(Mandatory=$true)]$Item)
$lcid = Get-LcidFromLanguageItem -Item $Item
if ($null -ne $lcid) {
return (Get-FriendlyCultureNameFromLcid -Lcid $lcid)
}$nameGuess = $null
try {
$nameGuess = Get-PropValue -Object $Item -Candidates @(“DisplayName”,”displayName”,”Name”,”name”,”LanguageName”,”languageName”)
} catch {}
if ($nameGuess) { return “$nameGuess” }return “Unknown language (unrecognized shape)”
}function Build-IsoCurrencyToEnglishNameMap {
$map = @{}$cultures = [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::SpecificCultures)
foreach ($c in $cultures) {
try {
$r = [System.Globalization.RegionInfo]::new($c.Name)
if (-not $r) { continue }$iso = $r.ISOCurrencySymbol
if ([string]::IsNullOrWhiteSpace($iso)) { continue }if (-not $map.ContainsKey($iso)) {
$map[$iso] = $r.CurrencyEnglishName
}
} catch {
# ignore
}
}return $map
}function Get-CurrencyCodeFromItem {
param([Parameter(Mandatory=$true)]$Item)if ($Item -is [string]) { return $Item }
return (Get-PropValue -Object $Item -Candidates @(
“IsoCurrencyCode”,”isoCurrencyCode”,
“CurrencyName”,”currencyName”,
“Name”,”name”,”Code”,”code”
))
}function Get-CurrencySymbolFromItem {
param([Parameter(Mandatory=$true)]$Item)
if ($Item -is [string]) { return $null }
return (Get-PropValue -Object $Item -Candidates @(“Symbol”,”symbol”,”CurrencySymbol”,”currencySymbol”))
}# —————————-
# Main
# —————————-$culture = Get-Culture
$uiCulture = [System.Globalization.CultureInfo]::CurrentUICulture
$region = [System.Globalization.RegionInfo]::new($culture.Name)Write-Host “Detected OS Culture: $($culture.Name) / UI: $($uiCulture.Name) / Region: $($region.EnglishName) ($($region.TwoLetterISORegionName))”
Write-Host “”$displayName = Read-Host -Prompt “Input your environment display name (e.g., ‘Contoso Dev Sandbox’)”
if ([string]::IsNullOrWhiteSpace($displayName)) { throw “DisplayName cannot be empty.” }Install-RequiredModule -Name “Microsoft.PowerApps.Administration.PowerShell”
Write-Host “Creating a session against the Power Platform API…”
Add-PowerAppsAccount -Endpoint prod# —– Location —–
Write-Host “Loading supported environment locations from Power Platform…”
$locations = @(Get-AdminPowerAppEnvironmentLocations)$defaultLocationIndex = Find-BestLocationDefaultIndex -Locations $locations -Region $region
$selectedLocation = Select-FromList `
-Title “Select LocationName (default suggested from your OS region)” `
-Items $locations `
-DefaultIndex $defaultLocationIndex `
-ToLabel {
param($loc)
$ln = Get-PropValue -Object $loc -Candidates @(“LocationName”,”locationName”)
$ld = Get-PropValue -Object $loc -Candidates @(“LocationDisplayName”,”LocationDisplayname”,”locationDisplayName”,”DisplayName”,”name”)
if ($ld) { “$ld ($ln)” } else { “$ln” }
}$locationName = (Get-PropValue -Object $selectedLocation -Candidates @(“LocationName”,”locationName”)).ToString()
# —– SKU —–
$skuValues = Get-ValidateSetValues -CommandName “New-AdminPowerAppEnvironment” -ParameterName “EnvironmentSku”
if (-not $skuValues -or $skuValues.Count -eq 0) {
$skuValues = @(“Sandbox”,”Production”,”Trial”)
}$defaultSkuIndex = 0
for ($i=0; $i -lt $skuValues.Count; $i++) {
if ($skuValues[$i].ToString().Equals(“Sandbox”, [System.StringComparison]::OrdinalIgnoreCase)) {
$defaultSkuIndex = $i; break
}
}$selectedSku = Select-FromList `
-Title “Select EnvironmentSku” `
-Items $skuValues `
-DefaultIndex $defaultSkuIndex `
-ToLabel { param($s) $s.ToString() }$environmentSku = $selectedSku.ToString()
# —– Dataverse —–
$provisionDatabase = Read-YesNo -Prompt “Provision Dataverse database?” -DefaultYes $true$templateUniqueName = $null
$templateMetadata = $null
$languageName = $null
$currencyName = $nullif ($provisionDatabase) {
# Templates
Write-Host “Loading Dataverse templates for location ‘$locationName’…”
$templates = @(Get-AdminPowerAppCdsDatabaseTemplates -LocationName $locationName)$defaultTemplateIndex = Find-DefaultIndexByPredicate -Items $templates -Predicate {
param($t)
$tCode = Get-PropValue -Object $t -Candidates @(“TemplateName”,”templateName”,”Name”,”name”)
return ($tCode -and $tCode.ToString().Equals(“D365_FinOps_Finance”, [System.StringComparison]::OrdinalIgnoreCase))
}$selectedTemplate = Select-FromList `
-Title “Select Dataverse Template (Templates parameter)” `
-Items $templates `
-DefaultIndex $defaultTemplateIndex `
-ToLabel {
param($t)
$tName = Get-PropValue -Object $t -Candidates @(“DisplayName”,”displayName”,”FriendlyName”,”friendlyName”)
$tCode = Get-PropValue -Object $t -Candidates @(“TemplateName”,”templateName”,”Name”,”name”)
if ($tName -and $tCode -and ($tName.ToString() -ne $tCode.ToString())) { “$tName ($tCode)” }
else { “$tCode” }
}$templateUniqueName = (Get-PropValue -Object $selectedTemplate -Candidates @(“TemplateName”,”templateName”,”Name”,”name”)).ToString()
# Languages (friendly + robust extraction)
Write-Host “Loading Dataverse languages for location ‘$locationName’…”
$languages = @(Get-AdminPowerAppCdsDatabaseLanguages -LocationName $locationName)$osLcid = $culture.LCID
$defaultLangIndex = Find-DefaultIndexByPredicate -Items $languages -Predicate {
param($l)
$lcid = Get-LcidFromLanguageItem -Item $l
return ($null -ne $lcid -and $lcid -eq $osLcid)
}$selectedLanguage = Select-FromList `
-Title “Select Dataverse Language (friendly name shown; LCID used under the hood)” `
-Items $languages `
-DefaultIndex $defaultLangIndex `
-ToLabel { param($l) Get-LanguageFriendlyLabel -Item $l }$selectedLcid = Get-LcidFromLanguageItem -Item $selectedLanguage
if ($null -eq $selectedLcid) {
throw “Could not determine LCID for selected language. Run: Get-AdminPowerAppCdsDatabaseLanguages -LocationName $locationName | Select -First 1 | Format-List *”
}
$languageName = $selectedLcid.ToString()# Currencies (FAST: one PP call + one local map build + one friendly list build)
Write-Host “Loading Dataverse currencies for location ‘$locationName’…”
$currenciesRaw = @(Get-AdminPowerAppCdsDatabaseCurrencies -LocationName $locationName)$isoNameMap = Build-IsoCurrencyToEnglishNameMap
$currencyChoices = @()
foreach ($c in $currenciesRaw) {
$code = (Get-CurrencyCodeFromItem -Item $c)
if ([string]::IsNullOrWhiteSpace($code)) { continue }$symbol = Get-CurrencySymbolFromItem -Item $c
$englishName = $null
if ($isoNameMap.ContainsKey($code)) { $englishName = $isoNameMap[$code] }$parts = @($code)
if (-not [string]::IsNullOrWhiteSpace($symbol)) { $parts += $symbol }
if (-not [string]::IsNullOrWhiteSpace($englishName)) { $parts += $englishName }$currencyChoices += [pscustomobject]@{
Code = $code
Label = ($parts -join ” — “)
Raw = $c
}
}$desiredIsoCurrency = $region.ISOCurrencySymbol
$defaultCurrencyIndex = 0
for ($i=0; $i -lt $currencyChoices.Count; $i++) {
if ($currencyChoices[$i].Code.Equals($desiredIsoCurrency, [System.StringComparison]::OrdinalIgnoreCase)) {
$defaultCurrencyIndex = $i
break
}
}$selectedCurrencyChoice = Select-FromList `
-Title “Select Dataverse Currency (default suggested from your OS region)” `
-Items $currencyChoices `
-DefaultIndex $defaultCurrencyIndex `
-ToLabel { param($x) $x.Label }$currencyName = $selectedCurrencyChoice.Code
# TemplateMetadata (FinOps post-provisioning packages)
$includeFinOpsMetadata = Read-YesNo -Prompt “Include FinOps post-provisioning package metadata (DevTools/DemoData)?” -DefaultYes $true
if ($includeFinOpsMetadata) {
$defaultDevTools = $environmentSku.Equals(“Sandbox”, [System.StringComparison]::OrdinalIgnoreCase)$devToolsEnabled = Read-YesNo -Prompt “Enable DevToolsEnabled?” -DefaultYes $defaultDevTools
$demoDataEnabled = Read-YesNo -Prompt “Enable DemoDataEnabled?” -DefaultYes $false$paramString = @(
“DevToolsEnabled=$($devToolsEnabled.ToString().ToLowerInvariant())”,
“DemoDataEnabled=$($demoDataEnabled.ToString().ToLowerInvariant())”
) -join “|”$templateMetadataObj = @{
PostProvisioningPackages = @(
@{
applicationUniqueName = “msdyn_FinanceAndOperationsProvisioningAppAnchor”
parameters = $paramString
}
)
}$templateMetadata = ($templateMetadataObj | ConvertTo-Json -Depth 10) | ConvertFrom-Json
}
}# —————————-
# Execute provisioning
# —————————-Write-Host “”
Write-Host “Provisioning new environment…”
Write-Host ” DisplayName : $displayName”
Write-Host ” LocationName : $locationName”
Write-Host ” EnvironmentSku : $environmentSku”
Write-Host ” ProvisionDatabase : $provisionDatabase”
if ($provisionDatabase) {
Write-Host ” Templates : $templateUniqueName”
Write-Host ” Language (LCID) : $languageName”
Write-Host ” Currency : $currencyName”
Write-Host (” TemplateMetadata : {0}” -f ($(if ($templateMetadata) { “included” } else { “none” })))
}$cmdParams = @{
DisplayName = $displayName
EnvironmentSku = $environmentSku
LocationName = $locationName
}if ($provisionDatabase) {
$cmdParams[“ProvisionDatabase”] = $true
$cmdParams[“LanguageName”] = $languageName # LCID like 1033
$cmdParams[“CurrencyName”] = $currencyName # ISO like USDif ($templateUniqueName) { $cmdParams[“Templates”] = @($templateUniqueName) }
if ($templateMetadata) { $cmdParams[“TemplateMetadata”] = $templateMetadata }
}try {
$result = New-AdminPowerAppEnvironment @cmdParams -WaitUntilFinished 0Write-Host “”
Write-Host “Environment provisioning initiated successfully!”
if ($null -ne $result) {
Write-Host “Result:”
$result | Format-List * | Out-String | Write-Host
}
}
catch {
Write-Host “”
Write-Host “Provisioning failed:”
Write-Host $_.Exception.Message
throw
}
Original Post https://www.atomicax.com/article/fo-environment-creation-script