Treat Your AI Agents Like Code - GitOps for Azure AI Foundry Agents
If your team builds AI agents in Azure AI Foundry, you have probably lived this story:
- Someone tweaks a prompt in the portal "just to test something".
- A week later the agent behaves differently and nobody knows why.
- There is no history, no review and no easy way back.
- The "dev" agent and the "prod" agent quietly drifted apart months ago.
Prompts, model choices, temperatures and tool definitions are code. They change behaviour in production just as much as a line of TypeScript does. So we started treating them that way.
This post walks through the setup: one Git repo as the single source of truth for every agent across several Foundry projects, where the only way to change production is a reviewed pull request.
The core idea
Every agent is a folder of plain files. A change reaches Foundry only through a reviewed PR. Completing the PR deploys it automatically. The PR review is the approval.
That's it. Everything else is tooling that makes this safe and painless.
1. Every agent is a folder
projects.json the Foundry projects: endpoint, API version, notes projects/ support-prod/ one folder per Foundry project agents/ HELPDESK-AGENT/ folder name = the agent name in Foundry, exactly instructions.md the prompt, word for word agent.json model, temperature, tools, description, metadata in-use.json optional: agents an app still depends on scripts/ pipelines/ schemas/ shared by every project Why split instructions.md from agent.json?
- Prompts are prose. Reviewing a prompt change as a clean Markdown line diff is far nicer than reviewing an escaped string inside JSON.
- Config is structure. Model, temperature, reasoning effort and tool definitions (including full OpenAPI specs) live in JSON and are validated against a schema.
A trimmed agent.json looks like this:
{ "definition": { "kind": "prompt", "model": "gpt-5", "reasoning": { "effort": "low" }, "tools": [ { "type": "web_search" }, { "type": "openapi", "openapi": { "name": "tickets_api", "spec": { "servers": [{ "url": "https://api.example.com?subscription-key={{secret:TICKETS_API_KEY}}" }] } } } ] }, "description": "Answers employee IT questions and opens tickets." } Notice the {{secret:...}} placeholder. More on that below.
2. Rules checked on every PR
A JSON Schema plus a small validator catch the mistakes that otherwise only show up as a cryptic API error during deploy:
-
kindmust beprompt. - Reasoning models (gpt-5, o-series) take no
temperature/top_p. -
description≤ 500 characters; OpenAPI tooldescription≤ 4000 characters. - Every project folder must exist in
projects.json, and the deploy pipeline's project list must match it.
Fail fast in the PR, not at 6 pm during a release.
3. Secrets never touch Git
Agents that call APIs often carry keys inside OpenAPI server URLs or headers. Committing those is a disaster, so the tooling redacts on the way in and restores on the way out:
- When an agent is pulled from Foundry, every secret-looking value (
subscription-key=,sig=,key=,code=, API-key headers...) is replaced with a placeholder like{{secret:TICKETS_API_KEY}}. - The placeholder name is derived from where the key is used (host + parameter). The same key always gets the same name, even after it's rotated.
- At deploy time, each placeholder gets the value the agent already has in live Foundry. A brand-new agent gets the value other agents in the same project use.
- Need to rotate or introduce a new key? Add a pipeline secret variable
SECRET_<NAME>; it wins over the live value. - A secret scanner runs on every PR and every backup. If anything secret-looking survives, the run fails.
The nice side effect: no extra secret store needed. Live Foundry already holds the values; the repo just holds the shape.
4. The developer workflow
edit files / import from a sandbox project │ ▼ commit to dev branch ──► PR to prod branch ──► PR check comments the Foundry diff │ reviewer completes it │ ▼ Deploy pipeline starts on its own (merge trigger) safety backup → preview → apply → read back & verify │ ▼ agent is live in its Foundry project There are three ways a change starts:
A. You built a new agent in the portal
Building in the portal is still great for experimenting. So keep doing that, in a sandbox project. When it's ready, run an Import pipeline:
- pick the source project and agent name,
- pick the target project (and optionally a new name),
- tick dry run the first time.
The pipeline copies the agent into projects/<target>/agents/<NAME>/, commits to the dev branch and opens the PR for you. Its summary warns about things that don't travel between projects:
- ⚠️ a secret the target project doesn't have yet,
- ❌ a connection (MCP server, AI Search index) that doesn't exist in the target,
- ⚠️ uploaded files / vector store IDs, which belong to the source project.
Changed it again in the sandbox? Re-run the import; the PR shows only what changed.
B. You edit the files directly
Branch, edit instructions.md or agent.json, run the local checks, push, open a PR:
npm ci npm test npm run validate npm run scan npm run diff -- --project support-prod --agents HELPDESK-AGENT C. You delete an agent
- First repoint or switch off the agent in whatever app calls it.
- Delete the folder in a PR.
- Deploy refuses to delete anything listed in the project's
in-use.json, which is a cheap but very effective seatbelt.
Foundry has no rename. A rename is two PRs: add the new folder, repoint the app, then remove the old folder.
5. The PR check: a human-readable Foundry diff
This is my favourite part. On every PR, a pipeline compares the files against live Foundry and posts a comment like:
support-prod / HELPDESK-AGENT (update) model: gpt-4.1 → gpt-5 temperature: 0.7 → (removed, reasoning model) instructions: +6 −2 lines tools: + web_search ~ tickets_api (spec changed) Reviewers see exactly what will change in production, not just which files changed. It sends nothing to Foundry; it only reads.
6. Merge = deploy
When the reviewer completes the PR into the prod branch, a merge trigger starts the deploy pipeline automatically. In Azure DevOps that is just:
trigger: branches: include: - main # your protected prod branch paths: include: - projects/* The deploy runs in auto mode over every agent folder the merge touched, in every project:
| Folder change | Action |
|---|---|
| new folder | create |
| changed folder | update (Foundry adds a new version) |
| removed folder | delete (always last) |
Each run:
- Takes a safety backup of the project first. No backup, no deploy.
- Previews the plan.
- Applies the changes.
- Reads every agent back and fails if Foundry doesn't match the files.
If any agent in a project is blocked (missing secret, missing connection, protected by in-use.json), nothing is sent to that project. All or nothing per project.
Rolling back? Revert the PR. The revert merges, and the same pipeline deploys the previous version.
A couple of guardrails worth copying:
- Scripts only write to Foundry with
--applyand an environment variable that only the deploy pipeline sets. You physically can't deploy from a laptop by accident. - Deploy runs only from the protected branch, so it can only ship what a reviewed PR put there.
7. Nightly backups and drift detection
People will still occasionally edit in the portal. Rather than pretending they won't, we detect it.
A backup pipeline runs every night and:
- discovers every Foundry project in the subscription automatically, so a project created today is backed up tonight with no code change,
- snapshots every agent (secrets redacted) to a separate
foundry-backupbranch, never to the prod branch, so a merged-but-not-yet-deployed change is never overwritten, - publishes the snapshot as a pipeline artifact,
- reports drift: agents whose live version differs from the files in the prod branch.
Drift means one of two things: a deploy that failed or was blocked, or a portal edit. The next deploy overwrites portal edits; if you want to keep one, copy it from the backup branch into a PR.
8. The four pipelines at a glance
| Pipeline | Triggered by | What it does | Writes to Foundry? |
|---|---|---|---|
| Import | developer | copies an agent from a sandbox project into the repo and opens the PR | no |
| PR Check | every PR into prod | tests, secret scan, schema checks, comments the Foundry diff | no |
| Deploy | PR completed (or manual) | safety backup → preview → apply → read-back verify | yes |
| Backup | nightly schedule | snapshots every agent of every project, reports drift | no |
Only one of the four can write, and it only runs from the protected branch.
9. One-time setup checklist
If you want to build something similar, here's what it took on the platform side:
- Branches: a dev branch for the team and a protected prod branch.
- Identity: give the pipeline's service connection the Foundry User role (formerly Azure AI User), ideally once at subscription scope so new projects are covered automatically. Note that an identity can't grant itself access, so someone with role-assignment rights does this once by hand.
- Pipelines: Import, PR Check, Deploy, Backup.
- Branch policy on prod: require a PR, at least one reviewer, and the PR Check as required build validation.
- Build service permissions: contribute only to the backup branch, plus permission to comment on PRs. No bypass policies anywhere.
- Agent pool: must reach every Foundry endpoint, with Node, git and the Azure CLI available.
What we gained
- Every prompt change is reviewed and has an author, a reason and a date.
- Rollback is a revert, not an archaeology session in the portal.
- Dev and prod can't silently drift, and when they do, the nightly report tells you.
- Secrets stay out of Git without adding another secret store.
- Promoting an agent from a sandbox to production is one pipeline run and one PR.
Lessons learned
- Split prose from config. Reviewing prompts as Markdown diffs changes how carefully people read them.
- Show the production diff, not the file diff. Reviewers care about "model goes from X to Y", not about which JSON key moved.
- Make the deploy verify itself. Reading every agent back after writing caught real issues that "HTTP 200" did not.
- Back up before every write. It's cheap and it turns scary deploys into boring ones.
- Assume portal edits will happen. Detect drift instead of hoping for discipline.
AI agents are quickly becoming production software. Giving them the same boring, reliable engineering practices as the rest of your stack — version control, code review, CI checks, automated deploys and backups — is one of the highest-leverage things you can do.
If you're running agents in Azure AI Foundry (or any agent platform with an API), I'd love to hear how you manage them. Drop a comment below! 👇