>_ The Manifest

The HR Onboarding Buddy works great on your machine: you hit F5, Copilot picks it up, new hires love it. Then your team lead asks “Can we deploy this to production with the real app registration?” and you’re staring at the manifest.json wondering how to manage two different app IDs without duplicating everything. This is exactly what the Agents Toolkit’s environment variable system solves.

How Environment Variables Work in ATK

Environment variables in the Microsoft 365 Agents Toolkit let you maintain a single set of configuration files: your manifest, your YAML pipeline, your agent definition: while swapping values per environment. Dev gets one app ID, production gets another, and you never duplicate a file.

The system has three parts: environment files that store the values, placeholders in your config files that reference them, and a CLI flag that picks which environment to use.

The env/ Folder

Every ATK project has an env/ folder at the root. Each file inside maps to an environment:

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

The .env.dev file holds non-secret configuration for your development environment:

# env/.env.dev
TEAMS_APP_ID=00000000-0000-0000-0000-000000000000
AAD_CLIENT_ID=11111111-1111-1111-1111-111111111111
API_BASE_URL=https://api-dev.contoso.com
TEAMSFX_ENV=dev

The .env.prod file holds the same variables with production values:

# env/.env.prod
TEAMS_APP_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
AAD_CLIENT_ID=22222222-2222-2222-2222-222222222222
API_BASE_URL=https://api.contoso.com
TEAMSFX_ENV=prod

Same variable names. Different values. One set of config files consuming them.

The ${{VAR_NAME}} Placeholder Pattern

ATK uses ${{VAR_NAME}} syntax to inject environment variables into your manifests and YAML files. This is different from standard shell $VAR or ${VAR} syntax: the double curly braces are ATK-specific.

Here’s the manifest.json for our HR Onboarding Buddy:

{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.24/MicrosoftTeams.schema.json",
  "version": "1.0.0",
  "manifestVersion": "1.24",
  "id": "${{TEAMS_APP_ID}}",
  "name": {
    "short": "HR Onboarding Buddy",
    "full": "HR Onboarding Buddy - Contoso"
  },
  "description": {
    "short": "Your friendly onboarding assistant",
    "full": "A declarative agent that helps new employees get started."
  },
  "developer": {
    "name": "Contoso",
    "websiteUrl": "${{API_BASE_URL}}"
  },
  "copilotAgents": {
    "declarativeAgents": [
      {
        "id": "declarativeAgent",
        "file": "declarativeAgent.json"
      }
    ]
  }
}

When you provision with --env dev, ${{TEAMS_APP_ID}} resolves to 00000000-0000-0000-0000-000000000000. With --env prod, it resolves to aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee. One manifest, two outcomes.

The same pattern works in m365agents.yml:

provision:
  - uses: teamsApp/create
    with:
      name: HR Onboarding Buddy-${{TEAMSFX_ENV}}
    writeToEnvironmentFile:
      teamsAppId: TEAMS_APP_ID

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

  - uses: teamsApp/update
    with:
      appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip

Notice ${{TEAMSFX_ENV}} in the output zip path. This keeps your dev and prod build artifacts separate: no accidental overwrites.

Secrets Management with .env.*.user Files

Any file ending in .user is gitignored by default. This is where client secrets, API keys, and other sensitive values belong.

# env/.env.dev.user
SECRET_AAD_CLIENT_SECRET=your-dev-client-secret-here
API_KEY=dev-api-key-12345

The convention is simple: shared config goes in .env.dev, secrets go in .env.dev.user. Both are loaded together when you target the dev environment: ATK merges them automatically.

⚠️ Warning

Never put secrets in .env.dev or .env.prod. Those files get committed to source control. The .user suffix exists specifically to keep sensitive values out of your repo. If you see a client secret in a non-.user file during code review, flag it immediately.

Switching Environments

In the CLI, you pass the --env flag:

# Provision to dev
atk provision --env dev

# Provision to production
atk provision --env prod

In VS Code, the Agents Toolkit sidebar shows an Environment dropdown. Select dev or prod and every toolkit action: provision, deploy, package: uses that environment’s variables.

💡 Tip

The TEAMSFX_ENV variable is set automatically based on which environment you select. You don’t define it yourself: ATK injects it. Use it freely in your YAML and manifests to differentiate between environments.

The One-YAML-File Rule

ATK uses two YAML files, and the split is intentional:

  • m365agents.yml: Used for all remote environments (dev, staging, prod). This is the file you version-control and share with your team.
  • m365agents.local.yml: Used exclusively for local development (F5 debugging). This file can include local-only steps like starting a dev server or setting up a local tunnel.

You don’t create separate YAML files per environment. The environment variable system handles the per-environment differences. One m365agents.yml serves dev, staging, and production: the ${{VAR_NAME}} placeholders resolve differently based on which --env you target.

📝 Note

If you need an environment-specific provisioning step (like creating an Azure resource only in prod), use conditional logic in your YAML action parameters rather than duplicating the entire file.

Why This Matters

The environment variable system in ATK is not just a convenience: it directly changes how safely and reliably you can ship agents to production.

Without it, every environment promotion becomes a manual find-and-replace exercise. You copy manifest.json, update the IDs, hope nothing gets missed, and commit environment-specific files that diverge over time. Configuration drift creeps in, someone accidentally deploys the dev app ID to production, and you spend an afternoon debugging an issue that was never a code problem.

With ATK’s environment system, your entire configuration is a single source of truth. Every environment reads from the same files. The only thing that changes is which .env.* file gets loaded. Promotions become a flag change, not a file surgery.

For teams, this compounds: new developers get a working local setup by filling in a single .env.dev.user file. They never touch manifests. They never misconfigure a shared app registration. Onboarding goes from a half-day struggle to a 15-minute setup.

For security, the .user file convention keeps secrets structurally separate from committed config. It is harder to accidentally commit a client secret when the tooling is designed to keep it out of the repository.

Best Practices

After deploying agents across multiple environments in production scenarios, here’s what I recommend:

  1. Keep variable names consistent: Use the same names across all .env.* files. If dev has API_BASE_URL, prod should too. Missing variables cause silent failures that are painful to debug.

  2. Document your variables: Add a comment block at the top of each .env file listing what each variable controls. Future you will appreciate this.

  3. Use .env.local for personal overrides: When you need a one-off config that’s just for your machine (your own test tenant, your personal API key), create an env/.env.local environment. It stays out of everyone else’s way.

  4. Never commit .user files: The .gitignore should already handle this, but verify it. Run git status after creating .user files to confirm they don’t show up.

  5. Add new variables to all environments at once: When you introduce a new variable, add it to every .env.* file immediately, even if you only have the dev value. An empty placeholder is better than a missing variable at deploy time.

Resources

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