ensure-pipelines-host

SkillDev tools

Ensures 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.

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 NoHost state — 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:

  1. 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.
  2. 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.
  3. eng.ms createcustompipelineshost (Microsoft-internal). Documents the Custom Host fast-path: a D365_ProjectHost org 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

ShapeHow it got thereWhere it livesOrg 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 geoD365_1stPartyAdminApps
Custom HostCreated 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 tenantD365_ProjectHost (or app-installed-onto-existing-env)
No host bound to source envTenant 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 getOrCreate endpoint 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 Link to 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 DefaultCustomPipelinesHostEnvForTenant setting. That setting is irreversible-adjacent (existing pipelines in the previous default become inaccessible — see learn.microsoft.com/power-platform/alm/set-a-default-pipelines-host). Out of scope.
  • Does not delete environments.
  • Does not write ProjectHostEnvironmentId directly. Binding is established through the documented Pipelines flow (creating a deploymentenvironment record 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:

  1. If a BAP token is provided, try BAP env-list / env-GET first (richer data including lastModifiedTime, permissions, tenantId).
  2. On HTTP 401 or 403, fall back to pac admin list --json via pac-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: D365DemoTSCE53051106 demo tenant rejects Az tokens for BAP even with correct audience claims).
  3. 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

  1. 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 via getOrCreate — Phase 5's WhoAmI call against instanceApiUrl triggers JIT before any subsequent host op. (For Custom Host paths the caller has access by construction.)
  2. CannotRedirect is a real terminal state, not a theoretical edge case. It happens when ProjectHostEnvironmentId (org setting on source env) points at PE but DefaultCustomPipelinesHostEnvForTenant (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.
  3. Admin-only Custom Host fast-path. PPAC's New custom host flow is gated by DeploymentHubCreatePipelinesHostForAdminsOnly and 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.
  4. 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.
  5. 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).
  6. 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 BAP getOrCreate endpoint 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. The provision-platform-host.js helper surfaces the distinction via an alreadyExisted: true | false flag in its return value (recorded in the docs/alm/last-host-check.json telemetry block as platformHostAlreadyExisted).

Prerequisites

  • PAC CLI logged in (pac env who succeeds)
  • Azure CLI logged in (az account show succeeds)
  • A source Dataverse environment URL (read from powerpages.config.json if 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:

  1. "Check local cache and detect prerequisites"
  2. "Run resolution order to find host"
  3. "Confirm action with user"
  4. "Execute chosen path"
  5. "JIT-provision and verify host"
  6. "Write host-check artifact"

Steps:

  1. Local cache fast-path. If docs/alm/last-host-check.json exists AND Date.now() - Date.parse(checkedAt) < cacheMaxAgeMs (default 24h; configurable via --cacheMaxAgeHours):

    • Acquire HOST_TOKEN for the cached finalHostEnvUrl origin.
    • 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 RESOLUTION from the cached file. Set ACTION_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.
    • If the file is missing, malformed, older than cacheMaxAgeMs, or contains ready: false → continue to Step 1.
    • Skip this step entirely if --no-cache is passed (used in CI / smoke tests).
  2. 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.

  3. 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 .siteName and .solutionManifest for messaging.

  4. Acquire BAP token (different audience than Dataverse):

    az account get-access-token --resource "https://service.powerapps.com/" --query accessToken -o tsv
    

    Store 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.

  1. 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_NAME is 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.

  1. Run microsoft_docs_search with the query: Power Platform Pipelines host environment Platform Host Custom Host.
  2. 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 via microsoft_docs_fetch.
  3. 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).
  4. 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 variableOur field
orgSetting.orgDbOrgSettingValueorgSettingHostEnvId
initialProjectHostEnvironmentId(same)
isInitialHostPlatformEnvironmentisPlatform
defaultCustomPipelinesHosttenantDefaultCustomHostEnvId
finalProjectHostEnvironmentIdfinalHostEnvId
projectHostStatusstatus

Steps:

  1. Org-setting probe (mirrors useGetOrgDbOrgSetting('ProjectHostEnvironmentId') line 103 in tsx). New helper check-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.
  2. Resolve env via BAP (mirrors useGetEnvironmentByName(initialProjectHostEnvironmentId) line 483 in tsx). New helper resolve-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. Set RESOLUTION.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 clear ProjectHostEnvironmentId and re-run, stop.
    • 403 → set RESOLUTION.status = "PermissionDenied", stop.
  3. If isPlatform === true, mirror the default-custom-tenant-setting check (lines 148–213 in tsx). Reuse the existing discover-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). Set RESOLUTION.status = "AvailableUsingPlatformHost".
    • found: true AND hostEnvUrl matches orgSettingHostEnvId → admin-default agrees with org setting. finalHostEnvId = orgSettingHostEnvId. Set RESOLUTION.status = "AvailableUsingCustomHostByAdminDefault".
    • found: true AND hostEnvUrl does NOT match orgSettingHostEnvIdCannotRedirect (Constraint 3). Set RESOLUTION.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. Set RESOLUTION.status = "AvailableUsingCustomHost". Skip Step 4–5; jump to Step 6.

  4. 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.

  5. Tenant env inventory + Pipelines-presence probe (decisional — feeds RESOLUTION.status). New helper list-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 reports environmentSku === 'Platform' and is included regardless. Pass --skus Production,Sandbox,Trial to include Trial envs (eligible for app-install via 4.B but not for env-create via 4.A — Trial-license tenants get NotEnoughCapacity_HasTrialLicense from env-create).
    • Sort remaining by lastModifiedTime desc.
    • 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."

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