>_ The Manifest

A platform team I was helping last month had the HR Onboarding Buddy live in production across three tenants (dev, staging, prod) and wanted to ship a v2 alongside the v1 that hundreds of new hires were already using. They asked me the same question I keep hearing: “Do we really need to maintain a copy of every manifest per environment, plus another set for v2?” No. The Agents Toolkit’s environment variable system handles both axes, target environment and agent version, with the same mechanism.

Two Axes, One System

When teams talk about “managing environments” for declarative agents, they usually mean one of two things, and often both at once:

  1. Target environments: the same agent deployed to different tenants or app registrations (dev, staging, prod, customer-specific tenants).
  2. Agent versions: multiple variants of the same agent running in parallel (v1 stable, v2 preview, an experimental branch).

The good news: in the Agents Toolkit, both are solved the same way. You define an environment file per combination, and the ${{VAR_NAME}} placeholders in your manifest, declarative agent file, and m365agents.yml resolve at provision time. If you want a refresher on the basics, see Managing Environment Variables in the M365 Agents Toolkit. This post is about the strategy on top.

Modeling Target Environments

Start with the most common axis. Every team has at least dev and prod. Many also have staging. The pattern is to give each one its own file in env/:

env/
├── .env.dev
├── .env.dev.user
├── .env.staging
├── .env.staging.user
├── .env.prod
└── .env.prod.user

Each file holds the same variables, with values appropriate for that target:

# env/.env.staging
TEAMS_APP_ID=33333333-3333-3333-3333-333333333333
AAD_CLIENT_ID=44444444-4444-4444-4444-444444444444
API_BASE_URL=https://api-staging.contoso.com
SHAREPOINT_SITE_URL=https://contoso.sharepoint.com/sites/hr-staging
AGENT_DISPLAY_NAME=HR Onboarding Buddy (Staging)
TEAMSFX_ENV=staging
💡 Tip

Bake the environment name into the agent’s display name for non-prod tenants. When a tester sees “HR Onboarding Buddy (Staging)” in their Copilot agents list, they know exactly which version they are talking to. This single change has saved more bug reports than any logging I have ever added.

Switching environments is then a flag:

atk provision --env staging
atk deploy --env staging
atk publish --env staging

That is the first axis solved. Now the interesting part.

Modeling Multiple Versions

Here is where most teams overthink it. Versioning a declarative agent does not require a new repo, a new branch strategy, or a fork. It requires another environment.

Treat each version as a deployment target. The HR Onboarding Buddy v2 going to the same prod tenant as v1? That is just prod-v2:

env/
├── .env.dev
├── .env.staging
├── .env.prod          # v1, the stable one
├── .env.prod-v2       # v2, running side by side
└── ...corresponding .user files

The .env.prod-v2 file points at a different Teams app ID and a different declarative agent ID, so the two agents coexist in the same tenant without colliding:

# env/.env.prod-v2
TEAMS_APP_ID=55555555-5555-5555-5555-555555555555
AAD_CLIENT_ID=22222222-2222-2222-2222-222222222222
API_BASE_URL=https://api.contoso.com
SHAREPOINT_SITE_URL=https://contoso.sharepoint.com/sites/hr
AGENT_DISPLAY_NAME=HR Onboarding Buddy (Preview)
AGENT_VERSION=2.0.0
TEAMSFX_ENV=prod-v2

Then your manifest uses the variables for everything that needs to differ:

{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.24/MicrosoftTeams.schema.json",
  "manifestVersion": "1.24",
  "id": "${{TEAMS_APP_ID}}",
  "version": "${{AGENT_VERSION}}",
  "name": {
    "short": "${{AGENT_DISPLAY_NAME}}",
    "full": "${{AGENT_DISPLAY_NAME}} - Contoso"
  },
  "developer": {
    "name": "Contoso",
    "websiteUrl": "${{API_BASE_URL}}"
  },
  "copilotAgents": {
    "declarativeAgents": [
      {
        "id": "declarativeAgent",
        "file": "declarativeAgent.json"
      }
    ]
  }
}

Same manifest. Two different installable apps. Both live in the same tenant. Users who got the preview see v2, everyone else stays on v1.

📝 Note

The Teams app ID is what makes this work. Two apps with different IDs are two completely separate installations from the platform’s perspective, even if they share most of their code. This is also how you do real A/B testing on agent personas without touching production users.

Branching the Agent Definition Itself

Sometimes the change between versions is bigger than a few values. Maybe v2 has different instructions, a new capability, or a different set of plugins. You have two clean options.

Option A: keep one declarativeAgent.json with branching variables. Works when the differences are small (different instructions paragraph, different SharePoint site).

Option B: use a different file per version, switched by a variable referenced directly from the Teams app manifest:

{
  "copilotAgents": {
    "declarativeAgents": [
      {
        "id": "declarativeAgent",
        "file": "declarativeAgent.${{AGENT_VARIANT}}.json"
      }
    ]
  }
}

Then in m365agents.yml you keep the package step generic, and let the variant flow through to the output artifact name:

provision:
  - uses: teamsApp/zipAppPackage
    with:
      manifestPath: ./appPackage/manifest.json
      outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip
      outputFolder: ./appPackage/build

With AGENT_VARIANT=v1 the package picks up declarativeAgent.v1.json. With AGENT_VARIANT=v2 it picks up declarativeAgent.v2.json. Both files live in your repo, both get reviewed in PRs, and you never have to maintain a feature flag inside a JSON file.

The output zip path also includes ${{TEAMSFX_ENV}}, so your build artifacts never overwrite each other. appPackage.prod.zip and appPackage.prod-v2.zip sit happily next to each other in ./appPackage/build/.

Wiring It Into CI/CD

The combination becomes powerful when you let your pipeline pick the environment. A simple matrix in GitHub Actions or Azure DevOps lets you provision every environment from one workflow:

strategy:
  matrix:
    include:
      - target: dev
        secret_name: AAD_SECRET_DEV
      - target: staging
        secret_name: AAD_SECRET_STAGING
      - target: prod
        secret_name: AAD_SECRET_PROD
      - target: prod-v2
        secret_name: AAD_SECRET_PROD_V2
steps:
  - uses: actions/checkout@v4
  - run: npm install -g @microsoft/m365agentstoolkit-cli
  - run: atk provision --env ${{ matrix.target }}
    env:
      SECRET_AAD_CLIENT_SECRET: ${{ secrets[matrix.secret_name] }}
  - run: atk deploy --env ${{ matrix.target }}

Each matrix job loads the right .env.* file and pulls its secret from the explicitly mapped GitHub secret. The mapping is needed because GitHub secret names only allow uppercase letters, digits, and underscores: a target like prod-v2 cannot share its name with the secret. Promoting a change from staging to prod is now a workflow trigger, not a merge ceremony.

⚠️ Warning

Never store production secrets in .env.prod. Always use .env.prod.user locally and your CI/CD secret store in pipelines. The .user files are gitignored on purpose, and your CI should inject SECRET_* variables at runtime.

A Simple Naming Convention

After helping a few teams scale this pattern, here is the naming I recommend for your env/ folder:

File patternPurpose
.env.<target>Tenant or stage: dev, staging, prod
.env.<target>-<variant>Version or branch within a target: prod-v2, prod-experimental
.env.<target>.userSecrets for that target, never committed
.env.localPersonal overrides for local F5 only

Stick to this and a new engineer can read your env/ folder and immediately know what ships where.

The Value You Just Unlocked

Moving from “one manifest per environment” to “one repo, many environment files” changes how your team operates:

  • Safe parallel versions: Run v1 and v2 in the same prod tenant for real-user pilots without forking your code.
  • Promotion is a flag: --env prod is the entire promotion process. No file edits, no merge gymnastics.
  • CI/CD becomes a matrix: One workflow ships every environment with the same steps, reducing drift between dev and prod to zero.
  • Onboarding new devs is minutes: A new teammate fills in .env.dev.user and they are productive. They never touch a manifest.
  • Auditable rollouts: Each environment has a single source of truth file. Reviewing what changed between prod and prod-v2 is a diff of two files, not a code archaeology session.

The transformation is simple. You stop thinking about “environments” and “versions” as separate concerns and start treating both as deployment targets. The tooling already supports this, you just have to lean into it.

Resources

Have questions or want to share what you're building? Connect with me on LinkedIn or check out more on The Manifest.