ensure-pipelines-host
SkillDev toolsEnsures the tenant has a usable Power Platform Pipelines host environment before any pipeline operation runs. Detects host state via the same resolution order as the Power Apps UI (org-db setting → BAP env metadata → default-custom-host setting); if any existing host (Platform or Custom) is found, uses it. If no host is bound to the source env, provisions a new **Platform Host** (recommended, idempotent) or a **Custom Host** via the BAP env-create API with the `D365_ProjectHost` template, or guides the user through PPAC install / `New custom host` (manual fallbacks). Polls lifecycle operations, verifies the host responds to Pipelines API calls, writes a host-check artifact other ALM skills consume. Use when asked to: "set up pipelines host", "ensure pipelines host", "no pipelines host", "install pipelines", "create pipelines host", "provision platform host", "provision custom host". Also invoked transparently by /power-pages:setup-pipeline when its host discovery step finds nothing.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the ensure-pipelines-host skill
What this skill tells your AI
The instructions your AI receives, as published by microsoft/power-platform-skills in plugins/power-pages/skills/ensure-pipelines-host/SKILL.md and read by ahel’s review.
Plugin check: Run
node "${PLUGIN_ROOT}/scripts/check-version.js"— if it outputs a message, show it to the user before proceeding.
ensure-pipelines-host
Scope: When no host is bound to the source env, this skill detects any existing host (Custom or PE) for reuse, or — in
NoHoststate — offers three provisioning paths: a new Platform Host (recommended; idempotent, ~3–5 min); a new Custom Host (admin-only, ~5–10 min); or PPAC manual provisioning (fallback). Implementation details — endpoint names, template names, BAP audience — live in Phase 4.0 / 4.A / 4.C below; user-facing prose stays focused on outcomes.
Power Platform Pipelines need a host environment — a Dataverse environment with the Power Platform Pipelines managed solution installed, where pipelines, stages, run history, and artifacts live. The existing setup-pipeline and deploy-pipeline skills assume a host is already configured. This skill closes that gap.
What we know (sources of truth)
This plan is grounded in three primary sources, in priority order:
useGetOrCreatePlatformEnvironment.v4.ts(Microsoft-internal client source —power-platform-ux/packages/powerapps-appdeployment-ux/src/hooks/v4/). Defines the exact HTTP contract for Platform Environment provisioning: endpoint, body, headers, polling.ProjectHostProvider.tsx(same repo,src/components/ProjectHostProvider/). Defines the exact resolution order the Power Apps UI uses to determine which environment is the project host for a source environment. We mirror that order so this skill agrees with the UI.- eng.ms
createcustompipelineshost(Microsoft-internal). Documents the Custom Host fast-path: aD365_ProjectHostorg template that ships the Pipelines app pre-installed, callable through the standard environment-creation API.
Public Microsoft Learn (learn.microsoft.com/power-platform/alm/{platform-host-pipelines, custom-host-pipelines, set-a-default-pipelines-host}) is the user-facing description of the same flows; we cite it for behaviors users will recognize. HARs in PipelinesDeployScenario.har and Pipelines.har confirm the read-side calls.
Three host shapes the tenant can be in
| Shape | How it got there | Where it lives | Org template |
|---|---|---|---|
| Platform Host (PE) | Auto-provisioned by getOrCreate BAP call (or as a side-effect of first navigation to the Pipelines page in make.powerapps.com). Hidden from the env picker. One per tenant. | Microsoft-managed Dataverse env in tenant's home geo | D365_1stPartyAdminApps |
| Custom Host | Created by an admin via PPAC Deployments → New custom host, or via the standard env-create API with the D365_ProjectHost template, or by installing the Power Platform Pipelines app on an existing Dataverse env. | A regular Dataverse env in the tenant | D365_ProjectHost (or app-installed-onto-existing-env) |
| No host bound to source env | Tenant has not used Pipelines from this env. | — | — |
The current discover-pipelines-host.js only checks the tenant-level DefaultCustomPipelinesHostEnvForTenant setting. That's one signal of many. This skill implements the full resolution order.
Resolution order (mirrors ProjectHostProvider.tsx)
This is the load-bearing decision tree. It is what the Power Apps UI does. We replicate it so the skill agrees with the UI.
┌─────────────────────────────────────────────────────────────────────┐
│ 1. GetOrgDbOrgSetting('ProjectHostEnvironmentId') on source env │
└──────────────────────────┬──────────────────────────────────────────┘
│
┌──────────────┴───────────────┐
│ value present │ value empty
▼ ▼
┌───────────────────────┐ ┌────────────────────────────┐
│ 2. Resolve env via │ │ 5a. Tenant-wide search: │
│ BAP GET │ │ list envs + per-env │
│ /environments/{id} │ │ /deploymentpipelines │
└───────┬───────────────┘ │ probe. │
│ │ │
environmentSku? │ - 1 Custom Host found → │
│ │ AvailableUnboundCustom │
┌────┴────────────┐ │ (3.C-pre) │
│ Platform │ │ - >1 Custom Hosts → │
│ │ │ MultipleUnboundCustom │
│ │ │ (3.C-pre') │
│ │ │ - PE only → │
│ │ │ PlatformHostExists- │
│ │ │ Unbound (3.C-pre'') │
│ │ │ - none → NoHost (3.C) │
│ │ │ │
│ │ │ 5b. Decision tree paths │
│ │ │ for create-new (3.C): │
│ │ │ - Platform getOrCreate │
│ │ │ (fast-path, no admin) │
│ │ │ - Custom D365_ProjectHost│
│ │ │ (fast-path, admin) │
│ │ │ - Manual app install │
│ │ │ - Manual PPAC create │
│ │ └────────────────────────────┘
▼ │
┌──────────────┐ │
│ 3. Check │ │ environmentSku ≠ Platform (Custom Host)
│ Default- │ ▼
│ Custom- │ ┌──────────────────────────────┐
│ Pipelines- │ │ 4. Use the Custom Host │
│ HostEnv- │ │ directly. Skip default- │
│ ForTenant │ │ custom check. │
└──────┬───────┘ └──────────────────────────────┘
│
┌───┴────────────────────────┐
│ admin set a custom default │
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────────┐
│ default == │ │ default != │
│ org setting? │ │ org setting │
│ │ │ │
│ → use default │ │ → CannotRedirect ERROR │
│ custom │ │ (user locked to PE │
└─────────────────┘ │ but admin overrode │
│ at tenant scope) │
└─────────────────────────┘
if no admin default → use PE
Source: ProjectHostProvider.tsx lines 100–213 (orgSetting fetch → defaultCustomPipelinesHost fetch → finalProjectHostEnvironmentId resolution).
What this skill does NOT do
These are deliberate non-goals (each based on a hard constraint or a destructive blast-radius — see Design Constraints below):
- Does not silently provision anything. Any action that creates an env or binds the source env to a host requires explicit user confirmation, with the tenant name + tenant ID echoed back. PE is tenant-singleton and admin-non-deletable, so the Phase 4.0 pre-call confirmation gate is the principal mitigation against wrong-tenant provisioning. The
getOrCreateendpoint is idempotent — calling it on a tenant that already has a PE returns the existing one rather than creating a duplicate. - Does not call
Force Linkto rebind an environment to a different host. Force Link is destructive (makers lose access to existing pipelines in the previous host) and is hidden behind a separate confirmation gate, only reachable when the user explicitly says "rebind". - Does not change the tenant-level
DefaultCustomPipelinesHostEnvForTenantsetting. That setting is irreversible-adjacent (existing pipelines in the previous default become inaccessible — seelearn.microsoft.com/power-platform/alm/set-a-default-pipelines-host). Out of scope. - Does not delete environments.
- Does not write
ProjectHostEnvironmentIddirectly. Binding is established through the documented Pipelines flow (creating adeploymentenvironmentrecord in the host); writing the org setting directly bypasses validation.
Auth strategy: PAC-first with BAP fallback (--source auto)
Read-side detection (Phase 2 resolution order, env list, env-by-id) defaults to --source auto:
- If a BAP token is provided, try BAP env-list / env-GET first (richer data including
lastModifiedTime,permissions,tenantId). - On HTTP 401 or 403, fall back to
pac admin list --jsonviapac-bap-shim.js. PAC has its own first-party client-ID grants on BAP that Az CLI doesn't always inherit (verified 2026-04-28:D365DemoTSCE53051106demo tenant rejects Az tokens for BAP even with correct audience claims). - If no BAP token is provided at all, go straight to PAC.
The PAC shim returns BAP-shaped data; downstream code (sku filter, ranking, classification) is unchanged. Fields not provided by PAC (tenantId, lastModifiedTime, permissions, isManaged) come back as null — none are critical for host detection. PAC also doesn't surface Platform Hosts (PE) since pac admin list doesn't include Platform-sku envs; PE detection requires --source bap with a working BAP token.
Write-side actions (env-create POST in provision-custom-host.js, lifecycle op polling) still require BAP. Az CLI tokens with the right audience usually work for these even when env-list calls fail, because the BAP RP enforces different policy on actions than reads. If provision-custom-host.js returns 401, the user must register a service principal in the target tenant (or use the PPAC UI fallback path 4.C).
Design Constraints
- JIT provisioning is required when a PE is selected — existing or freshly provisioned. From
ProjectHostProvider.tsx(line 232–240 comment): "In the Platform Environment case, the user may not already be provisioned there, so BAP cannot discover it. So we'll use the org URL we retrieve from the getOrCreate call to make this first request so that user JIT can be triggered." When Phase 2 detects an existing PE and the user accepts it (Phase 3.A) — or when Phase 4.0 provisions a new PE viagetOrCreate— Phase 5'sWhoAmIcall againstinstanceApiUrltriggers JIT before any subsequent host op. (For Custom Host paths the caller has access by construction.) CannotRedirectis a real terminal state, not a theoretical edge case. It happens whenProjectHostEnvironmentId(org setting on source env) points at PE butDefaultCustomPipelinesHostEnvForTenant(admin tenant setting) points elsewhere. The skill must detect this and surface it as a specific error — falling through silently would route pipeline ops at the wrong host.- Admin-only Custom Host fast-path. PPAC's
New custom hostflow is gated byDeploymentHubCreatePipelinesHostForAdminsOnlyand shows only for Global / Power Platform / Dynamics admins (eng.ms doc). The BAP env-create API also needs the equivalent privilege. Non-admins get 403; the skill preflight-attestation-prompts and gracefully falls back to manual paths. - 404 from BAP env GET is ambiguous. Returns 404 for deleted, disabled, no-PE, and no-access without distinguishing (
PowerPipelines_PE_Knowledge.md§6.A). We never treat a single 404 as "no host exists" — we corroborate via list-environments and the org setting before acting. - Each environment is bound to only one host at a time. Rebinding requires Force Link, which is destructive in the previous host. Out of scope (see non-goals).
- The skill runs in user OAuth context — same scope and audience the Power Apps UI uses. BAP calls use
https://service.powerapps.com/audience.
Idempotency of
getOrCreate— the BAPgetOrCreateendpoint is idempotent (existing PE returns 200 +provisioningState === 'Succeeded'; new PE returns 202 + lifecycle op). Phase 4.0 leverages this — calling getOrCreate on a tenant that already has a PE is safe and just returns the existing one. Theprovision-platform-host.jshelper surfaces the distinction via analreadyExisted: true | falseflag in its return value (recorded in thedocs/alm/last-host-check.jsontelemetry block asplatformHostAlreadyExisted).
Prerequisites
- PAC CLI logged in (
pac env whosucceeds) - Azure CLI logged in (
az account showsucceeds) - A source Dataverse environment URL (read from
powerpages.config.jsonif invoked from a Power Pages project; passed as arg otherwise) - For Phase 4 admin-only paths: caller has Global / Power Platform / Dynamics admin (skill detects and surfaces 403 cleanly if missing)
Phases
Phase 1 — Detect prerequisites and gather tenant context
Create all tasks upfront at the start of this phase.
Tasks to create:
- "Check local cache and detect prerequisites"
- "Run resolution order to find host"
- "Confirm action with user"
- "Execute chosen path"
- "JIT-provision and verify host"
- "Write host-check artifact"
Steps:
-
Local cache fast-path. If
docs/alm/last-host-check.jsonexists ANDDate.now() - Date.parse(checkedAt) < cacheMaxAgeMs(default 24h; configurable via--cacheMaxAgeHours):- Acquire
HOST_TOKENfor the cachedfinalHostEnvUrlorigin. - One cheap probe:
GET {finalHostEnvUrl}/api/data/v9.0/solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor'&$select=version&$top=1(proves Pipelines is installed AND captures version in one round-trip)- 200 → cache is valid. Set
RESOLUTIONfrom the cached file. SetACTION_TAKEN = "none". Skip Phases 2–5; jump to Phase 6 with a "reused cached host" summary. - 404 / 403 / timeout / network → cache is stale or no longer accessible. Continue to Step 1 (full resolution). Do NOT fail — stale cache is expected after env deletion or permission changes.
- 200 → cache is valid. Set
- If the file is missing, malformed, older than
cacheMaxAgeMs, or containsready: false→ continue to Step 1. - Skip this step entirely if
--no-cacheis passed (used in CI / smoke tests).
- Acquire
-
Run
verify-alm-prerequisites.js:node "${PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js"Capture
.envUrl(devEnvUrl),.token(DEV_TOKEN),.userId,.tenantId,.organizationId. Stop on auth failure with the script's remediation message. -
Run
detect-project-context.js(non-fatal — skill is also valid outside a Power Pages project):node "${PLUGIN_ROOT}/scripts/lib/detect-project-context.js"Capture
.siteNameand.solutionManifestfor messaging. -
Acquire BAP token (different audience than Dataverse):
az account get-access-token --resource "https://service.powerapps.com/" --query accessToken -o tsvStore as
BAP_TOKEN. This is used by all BAP/providers/Microsoft.BusinessAppPlatform/...calls in Phases 2 and 4.
3a. Resolve tenant display name (one-shot, best-effort). Phase 1.4 and Phase 4.0 echo a human-readable tenant name alongside the tenant GUID so the user can verify the target tenant. Acquire it from the Microsoft Graph organization endpoint:
az rest --method GET --url "https://graph.microsoft.com/v1.0/organization?$select=id,displayName" --resource "https://graph.microsoft.com/" --query "value[0].displayName" -o tsv
Store as TENANT_DISPLAY_NAME. On any failure (no Graph permission, network error, multi-tenant ambiguity), fall back to TENANT_DISPLAY_NAME = null and continue — Phase 1.4 / 4.0 prompts handle a null display name by showing the tenant GUID alone.
🚦 Gate (consent · ensure-pipelines-host:1.4.tenant-identity): Echo tenant display name + tenant GUID + dev env URL before any host detection. First of the wrong-tenant guards. Cancel exits cleanly before any BAP/Dataverse call.
-
Tenant identity confirmation gate. Echo back via
AskUserQuestion:"About to inspect Pipelines host configuration for tenant {TENANT_DISPLAY_NAME} (
{tenantId}), org{organizationId}, dev env{devEnvUrl}. Continue? 1. Yes / 2. Cancel"(When
TENANT_DISPLAY_NAMEis null, drop the bold tenant-name segment and lead with the tenant GUID.)First of the consent gates that guard against wrong-tenant operations.
Phase 1.5 — Ground in current Pipelines host documentation
Reference:
${PLUGIN_ROOT}/references/alm-docs-grounding.md
Cap this step at ~30 seconds. If MCP search / fetch errors out, log a one-line note and continue — this skill must remain runnable offline.
- Run
microsoft_docs_searchwith the query:Power Platform Pipelines host environment Platform Host Custom Host. - Fetch
https://learn.microsoft.com/en-us/power-platform/alm/pipelines(and at most one sister page on host setup, default-custom-host configuration, or admin role requirements) in parallel viamicrosoft_docs_fetch. - Extract a one-paragraph summary of what Microsoft Learn currently says about Platform vs Custom Host trade-offs, the resolution order (org-db setting → BAP env metadata → tenant default), and admin role requirements. Compare against this skill's own Resolution order section and
${PLUGIN_ROOT}/references/cicd-pipeline-patterns.md; flag any divergence (e.g. new Platform-Host SKU, changed default-custom-host setting name, new tenant policy controls). - Use the summary to inform Phase 2+ decisions. Do not silently change skill behavior — surface any divergence to the user as a soft warning before Phase 3 (Confirm action with user).
Phase 2 — Run resolution order to find host
This phase is read-only. It produces a RESOLUTION object the user-confirm phase branches on.
The phase mirrors ProjectHostProvider.tsx exactly. The useState variables in that hook map to fields in our RESOLUTION:
| TS variable | Our field |
|---|---|
orgSetting.orgDbOrgSettingValue | orgSettingHostEnvId |
initialProjectHostEnvironmentId | (same) |
isInitialHostPlatformEnvironment | isPlatform |
defaultCustomPipelinesHost | tenantDefaultCustomHostEnvId |
finalProjectHostEnvironmentId | finalHostEnvId |
projectHostStatus | status |
Steps:
-
Org-setting probe (mirrors
useGetOrgDbOrgSetting('ProjectHostEnvironmentId')line 103 in tsx). New helpercheck-env-host-binding.js:POST {devEnvUrl}/api/data/v9.0/GetOrgDbOrgSetting Authorization: Bearer {DEV_TOKEN} Body: { "SettingName": "ProjectHostEnvironmentId" }- Empty
SettingValue→ no current binding. Skip to Step 4. - Non-empty → store as
orgSettingHostEnvId. Continue to Step 2.
- Empty
-
Resolve env via BAP (mirrors
useGetEnvironmentByName(initialProjectHostEnvironmentId)line 483 in tsx). New helperresolve-env-by-id.js:GET https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments/{envId}?api-version=2020-06-01&$expand=properties.linkedEnvironmentMetadata,properties.permissions Authorization: Bearer {BAP_TOKEN}- 200 → capture
environmentSku,displayName,linkedEnvironmentMetadata.instanceApiUrl,linkedEnvironmentMetadata.instanceUrl. SetRESOLUTION.isPlatform = (environmentSku === 'Platform'). - 404 → disambiguate before acting (Constraint 5). Run
list-tenant-envs.js(Step 5) and check whether the env is in the list:- If listed → user lacks access → set
RESOLUTION.status = "PermissionDenied", surface to user, stop. - If not listed → env is genuinely deleted/disabled → set
RESOLUTION.status = "OrgSettingStale", recommend the user clearProjectHostEnvironmentIdand re-run, stop.
- If listed → user lacks access → set
- 403 → set
RESOLUTION.status = "PermissionDenied", stop.
- 200 → capture
-
If
isPlatform === true, mirror the default-custom-tenant-setting check (lines 148–213 in tsx). Reuse the existingdiscover-pipelines-host.js:node "${PLUGIN_ROOT}/scripts/lib/discover-pipelines-host.js" \ --envUrl "{devEnvUrl}" --token "{DEV_TOKEN}" --userId "{userId}"found: false→ tenant has no admin default custom host.finalHostEnvId = orgSettingHostEnvId(the PE). SetRESOLUTION.status = "AvailableUsingPlatformHost".found: trueANDhostEnvUrlmatchesorgSettingHostEnvId→ admin-default agrees with org setting.finalHostEnvId = orgSettingHostEnvId. SetRESOLUTION.status = "AvailableUsingCustomHostByAdminDefault".found: trueANDhostEnvUrldoes NOT matchorgSettingHostEnvId→CannotRedirect(Constraint 3). SetRESOLUTION.status = "CannotRedirect", capture both URLs. Stop with the specific error message — only an admin can resolve this.
If
isPlatform === false(Custom Host): use directly.finalHostEnvId = orgSettingHostEnvId. SetRESOLUTION.status = "AvailableUsingCustomHost". Skip Step 4–5; jump to Step 6. -
No org setting → tenant-wide search before declaring NoHost. Source env isn't bound, but a usable Custom Host may already exist in the tenant (admin-created, or created by a prior run of this skill in another project). Always inventory before offering to create.
-
Tenant env inventory + Pipelines-presence probe (decisional — feeds
RESOLUTION.status). New helperlist-tenant-envs.js:Step 5a — list envs:
GET https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments?api-version=2020-06-01&$expand=properties.linkedEnvironmentMetadata Authorization: Bearer {BAP_TOKEN}For each env capture
{ envId, displayName, environmentSku, instanceApiUrl, isManaged, hasDataverse: !!instanceApiUrl }.Step 5b — Pipelines-presence probe per env (parallel, max 10 concurrent; bounded by sku filter + maxEnvsToProbe cap):
Pre-filter (avoid probing every env in large tenants — recon found tenants with 1000+ envs):
- Skip envs without Dataverse (
linkedEnvironmentMetadata.instanceApiUrl == null). - Skip envs not in
--skus(default:Production,Sandbox— both are valid hosts for the Pipelines app via Phase 4.B install-on-existing). PE always reportsenvironmentSku === 'Platform'and is included regardless. Pass--skus Production,Sandbox,Trialto include Trial envs (eligible for app-install via 4.B but not for env-create via 4.A — Trial-license tenants getNotEnoughCapacity_HasTrialLicensefrom env-create). - Sort remaining by
lastModifiedTimedesc. - Cap at
--maxEnvsToProbe(default 50; covers the typical-tenant 80% case in <5s with 10-concurrent). - If cap is reached and no host found, surface a warning:
"Scanned N of M envs (filter: Production+Sandbox, sorted by lastModifiedTime). Pass --maxEnvsToProbe N+ or --skus Production,Sandbox,Trial to widen."
- Skip envs without Dataverse (
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 859
- Forks
- 176
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
ensure-pipelines-host- Source
- github.com/microsoft/power-platform-skills