The Governed Way to Deploy Copilot Studio Agents

Published by

on

How I extended a Copilot Studio deployment pipeline with a human approval step — and the security roles that make it actually safe.

Everyone’s building Copilot Studio agents. Far fewer talk about how you ship them safely — especially in the enterprise. This guide walks through adding a human approval gate to a Power Platform deployment pipeline, so a solution only promotes to UAT or prod once someone signs off.

A common question with pipelines is environment access: does a maker need rights in prod to deploy there? A maker only needs the baseline Environment Maker role on a target they deploy to — never System Administrator or elevated rights — because the service principal handles the privileged part of the import. Here’s the full setup.

📦 Grab the solution: the exported flow, the full security-role matrix, and setup steps are on GitHub — github.com/ifiecas/pp-pipelineapproval.

The shape of it

A maker hits Deploy in their dev environment. Instead of the solution sailing straight into UAT, the pipeline pauses at a gate, an approver gets an actionable card in Teams and email, and only once they approve does the solution promote — deployed by a service principal, not the maker.

Two ingredients make this work: a custom host environment, and a delegated deployment driven by an approval flow. Let’s walk through both.


Step 1 — A dedicated custom host environment

When you first create a pipeline, Power Platform can spin up a default host for you. For anything enterprise, I stand up a dedicated custom host environment instead — a single, purpose-built environment that holds all the pipeline configuration, the service principal, and the approval flow.

Why it’s worth the extra step: one governed place to manage who can run what, controlled access, and room to extend pipelines org-wide. The CS pipeline admin owns it, and that’s where all roles and permissions live.

One thing to know: the host doesn’t have to be a Managed Environment, but every target environment (UAT, prod) does.


Step 2 — The approval flow

Inside the host, I built a cloud flow that hooks into the pipeline’s extension events. The anatomy is simple:

  • Trigger: the Dataverse When an action is performed trigger, on the OnPreDeploymentStarted event. (You enable this by turning on Pre-deployment Step Required on the pipeline stage — that’s what inserts a gated step and holds it pending.)
  • Start and wait for an approval: the Approvals connector sends the approver a card, natively, in both Teams and email. No separate email connector needed.
  • A condition on the outcome: approve or reject.
  • Signal the pipeline back with the Dataverse action UpdatePreDeploymentStepStatus — status 20 to approve (pipeline continues) or 30 to reject (deployment stops).
  • Post a Teams card to the requestor so they know the decision.

The one gotcha future-me will thank present-me for: StageRunId binds to triggerOutputs()?['body/InputParameters/StageRunId'], and you have to set it independently in both the approve and reject branches.


Step 3 — Delegated deployment (keeping makers out of admin)

Delegated deployment is what keeps the maker’s access to a minimum. On the pipeline stage, turn on Is delegated deployment and point it at the service principal.

Now the privileged import — plug-ins, code components, and ownership of everything deployed — runs as the service principal’s identity, not the maker’s. That means the maker’s own role in the target can stay at the baseline Environment Maker level: enough to deploy to that stage, but never System Administrator or elevated rights in prod. That’s the difference that matters — makers get self-service without becoming prod admins.


The security roles that make it safe

This is the bit worth getting exactly right, so here’s the whole picture.

The service principal (the deploying identity):

  • In the hostDeployment Pipeline Administrator. Full read/write on the pipeline tables, and this is the identity the flow’s Dataverse connection uses to call UpdatePreDeploymentStepStatus.
  • In each target (UAT, prod) — System Administrator. Delegated deployment imports plug-ins and code components, and lower roles can’t deploy those.

The maker / requestor:

  • In the hostDeployment Pipeline User. Lets them run pipelines shared with them. They can’t even see the host in the environment picker, and it grants them no elevated access.
  • In dev — System Customizer or Environment Maker, to export solutions.
  • In UAT / prodEnvironment Maker. This is the baseline maker role, and it’s what a maker needs to deploy to a stage that targets that environment. Crucially, it’s not System Administrator and not elevated access — the service principal handles the privileged import. That’s the whole point: makers get the minimum role, never admin.

One detail people miss: the flow’s three connections shouldn’t all run as the service principal. Only Dataverse does. The Approvals and Teams connectors need an interactive, licensed identity — so I run those on a dedicated licensed service account, which keeps the flow from breaking when someone leaves.

The payoff

The maker gets a clean, one-click, self-service deployment. The approver gets a real decision point with everything they need to make the call. And prod stays locked down where it counts — makers only ever hold the baseline Environment Maker role, never admin rights, and nothing ships without a human approving it.

That separation is the governance win. It’s the boring, unglamorous plumbing that turns a Copilot Studio demo into something you’d actually run in an enterprise.

I’ll be honest — I mostly wrote this up so future-me remembers how I did it. If it saves you some time too, subscribe over at ifiecas.com, where I write up the AI and governance things I’m building as I go.


Discover more from Ivy Fiecas-Borjal

Subscribe to get the latest posts sent to your email.

Discover more from Ivy Fiecas-Borjal

Subscribe now to keep reading and get access to the full archive.

Continue reading