Version History for Your Copilot Studio Agents — Automatically

Published by

on

A guide for future me (and anyone else who wants to have version control for their Copilot Studio agents).

The problem I was solving

Here’s a question that stumps most teams building agents in Copilot Studio:

Not what it looks like now — what it looked like before the last few rounds of changes. Which topics existed. What the greeting said. What that trigger phrase was before someone tweaked it.

For most teams, the honest answer is: nobody knows.

Think about how you work on a Word document. If someone changes a paragraph, Word quietly keeps every version. You can see who changed what and when, and restore last Tuesday’s draft if today’s edits went sideways.

Now imagine Word without version history. One file, every edit overwrites the last. That’s how most Copilot Studio agents are built today. The tool makes building wonderfully easy — tweak, publish, done — but the only version of your agent that exists is the current one. No history, no diffs, no rollback, no answer to “what changed?”

That gap starts to hurt the moment your agent matters: when it behaves oddly after an update, when an auditor asks who changed what, or when you want to undo a change and realise there’s nothing to undo to.

The idea

Every time an agent is deployed to testing, a snapshot automatically saves into version history — timestamped, tied to the deployment, tied to the approval. The maker deploys exactly as before. Nobody learns a new tool, nobody remembers to back anything up. The record-keeping is a side effect of deploying.

The plumbing, in one breath: Power Platform Pipelines fires an event when a deployment completes → a Power Automate flow catches it → the flow queues an Azure DevOps pipeline → the pipeline exports the solution from dev, unpacks it into readable files, and commits it to a git repo.

Maker deploys → approval (if configured) → deployment succeeds
→ OnDeploymentCompleted event → flow → DevOps pipeline
→ export → unpack → commit → history 📚

Fully dynamic, too — the solution name travels through the event, so every agent deployed through the pipeline gets versioned. Add new agents, never touch the setup.

The build, in order

Do it in this sequence and each step feeds the next. (Full YAML and a sample event payload are in the GitHub repo.)

On the Azure DevOps side:

  1. Service connection → New → Power Platform type. It wants four things: your dev environment URL (admin center → environment → Environment URL), plus the service principal’s tenant ID, client ID, and secret from its Entra app registration. Whatever you name this connection, the YAML must reference it by that exact name.
  2. SP permissions → the service principal needs an application user in dev with System Customizer — it exports from there. (If your deployment setup marks this role “optional,” it’s not optional anymore.)
  3. Let the pipeline push → Project Settings → Repositories → your repo → Security → Build ServiceContribute: Allow. The step everyone forgets; without it the final git push dies.
  4. Add the YAML, create the pipeline → commit commit-solution.yml, then Pipelines → New → Existing YAML → Save (not Run). Grab the pipeline’s numeric ID from the URL: _build?definitionId=N.
  5. Make the variable settable → Edit pipeline → Variables → add solutionName → tick “Let users override this value when running this pipeline.”

On the Power Platform side:

  1. Build the flow in the host environment — the one where the Pipelines app is installed. Automated cloud flow, Dataverse trigger “When an action is performed”, action OnDeploymentCompleted. It only fires on successful deploys, so failed and rejected deployments never get committed — for free.
  2. Condition → one row: triggerOutputs()?['body/OutputParameters/DeploymentStageName'] equals your stage’s exact display name. That’s it. No solution-name check — leaving it out is what keeps this dynamic.
  3. If yes → Queue a new build → your org, project, the numeric definition ID from step 4, branch main, and Parameters: {"solutionName": "@{triggerOutputs()?['body/OutputParameters/ArtifactName']}"}
  4. Test end to end → tweak an agent, deploy, approve, then check: flow history → pipeline run → the commit. Open the diff and see your exact change in plain text. 🎉

The five traps (dear future me: read this part)

Every one of these cost me a debugging round. In the order I hit them:

  1. Wrong environment = no trigger. If the trigger’s Catalog dropdown doesn’t show the Pipelines catalog, you’re not in the host environment. The flow must live where the Pipelines app is installed — check the environment picker before building anything.
  2. The good fields are in OutputParameters, not InputParameters. ArtifactName and DeploymentStageName live under body/OutputParameters/.... Point the condition at InputParameters and it silently evaluates false forever — the flow “succeeds” and skips the build every time. When in doubt: run one deploy, open the trigger’s raw Outputs in run history, and copy the exact keys. Never guess.
  3. Variables, not YAML parameters. The DevOps connector sends queue-time variables. Declare parameters: in the YAML and queuing fails with “You can’t set the following variables (solutionName).” Use $(solutionName) in the YAML + the settable-at-queue-time checkbox.
  4. Definition ID ≠ build ID ≠ pipeline name. The flow wants the number from _build?definitionId=N. A run’s buildId or the pipeline’s display name both fail at runtime.
  5. The git push permission. Contribute rights for the Build Service on the repo. Two clicks, and forgetting them wastes twenty minutes at the very last step.

Bonus: a pipeline run ending in “No changes to commit” is not a bug — it means the repo already matches dev exactly (a redeploy with no edits). Reruns are idempotent by design.

For production (not the demo)

The demo runs on “my email works.” Production runs on no individual being load-bearing: the DevOps connection moves to a dedicated licensed service account, the flow’s ownership moves to that account or a team, and everything runs least-privilege (the DevOps account only needs to queue this one pipeline; the SP only needs System Customizer in dev).

The principle

If I had to boil this down to one sentence: good governance is invisible. The version that works isn’t the one where people follow rules — it’s the one where the right thing happens as a side effect of what people were already doing. Makers deploy exactly like they did before. The history writes itself.

Full setup guide, ready-to-use YAML, and the sample event payload: https://github.com/ifiecas/pp-autocommit/tree/main


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