earl 0.5.2

AI-safe CLI for AI agents
---
title: Environments
icon: Layers
description: Switch between production, staging, and other environments in Earl templates.
---

Environments let a single template target different backends — typically a production API and a staging one — without duplicating the command logic. You define named environments at the provider level, then optionally override the full operation for specific environments in each command.

## How it works

There are two pieces:

1. **The `environments` block** at the top of the template defines the available environments and their values (usually base URLs). This is where you set the default.
2. **`environment_overrides` blocks** inside individual commands replace the entire operation when a specific environment is active.

The two pieces are independent. You can use `environments` without any `environment_overrides`, and just interpolate `{{ vars.base_url }}` into your operation URL. Or you can skip the top-level block and use `environment_overrides` to swap operations entirely.

## The `environments` block

This goes at the top of the template file, before any `command` blocks:

```hcl
environments {
  default = "production"
  secrets = ["github.token"]
  production {
    base_url = "https://api.github.com"
  }
  staging {
    base_url = "https://staging.github.internal"
  }
}
```

When a command runs, the active environment's fields are available as `{{ vars.<key> }}`. In the example above, `{{ vars.base_url }}` resolves to `https://api.github.com` in production and `https://staging.github.internal` in staging.

`secrets` lists secrets that apply to all environments. Individual environments can add their own fields — if staging uses a different token, you can put that key name in the staging entry:

```hcl
environments {
  default = "production"
  secrets = ["github.token"]
  production {
    base_url = "https://api.github.com"
  }
  staging {
    base_url  = "https://staging.github.internal"
    token_key = "github.staging_token"
  }
}
```

The top-level `secrets` list is for secrets shared across all environments. Per-environment values in the `environments { }` inner block are data fields that end up in `vars.*`, not automatic secret references.

## Using `{{ vars.* }}` in operations

Once you have an `environments` block, use `vars.<key>` anywhere in the operation:

```hcl
operation {
  protocol = "http"
  method   = "GET"
  url      = "{{ vars.base_url }}/search/repositories"

  auth {
    kind   = "bearer"
    secret = "github.token"
  }
}
```

This is the simplest form of environment switching: one operation, parameterized by environment values.

## `environment_overrides`

When the difference between environments is more than a URL — different auth, different protocol, different headers — you can replace the entire operation for a specific environment:

```hcl
command "search_repos" {
  title   = "Search repositories"
  summary = "Search GitHub repos by query"
  description = "Search GitHub repositories."

  annotations {
    mode    = "read"
    secrets = ["github.token", "github.staging_token"]
  }

  param "query" {
    type        = "string"
    required    = true
    description = "Search query"
  }

  operation {
    protocol = "http"
    method   = "GET"
    url      = "{{ vars.base_url }}/search/repositories"

    auth {
      kind   = "bearer"
      secret = "github.token"
    }

    query = { q = "{{ args.query }}" }
  }

  environment_overrides {
    staging {
      operation {
        protocol = "http"
        method   = "GET"
        url      = "https://staging.github.internal/search/repositories"

        auth {
          kind   = "bearer"
          secret = "github.staging_token"
        }

        query = { q = "{{ args.query }}" }
      }
    }
  }
}
```

When the active environment is `staging`, Earl uses the override operation and ignores the default one entirely. For all other environments, the default operation runs.

The override is a complete replacement, not a merge. If the default operation has headers or query params you want to keep in staging, you need to repeat them.

## Activating an environment

Pass `--env` to `earl call`:

```bash
earl call --env staging github.search_repos --query "rust"
```

Or set a default in `~/.config/earl/config.toml`:

```toml
[environments]
default = "staging"
```

The `--env` flag overrides the config file default. The config file default overrides the template's `default` value.

## Protocol switching

By default, Earl rejects environment overrides that switch the protocol (e.g., from `http` to `grpc`). This prevents silent behavior changes when swapping environments.

If you need to switch protocols between environments, set the annotation:

```hcl
annotations {
  allow_environment_protocol_switching = true
}
```

Without this, an override that changes `protocol` will cause Earl to error when the template loads.

## Complete example

```hcl
version    = 1
provider   = "github"
categories = ["scm"]

environments {
  default = "production"
  secrets = ["github.token"]
  production {
    base_url = "https://api.github.com"
  }
  staging {
    base_url = "https://staging.github.internal"
  }
}

command "search_repos" {
  title       = "Search repositories"
  summary     = "Search GitHub repos by query"
  description = "Search GitHub repositories using GitHub's search API."

  annotations {
    mode    = "read"
    secrets = ["github.token"]
  }

  param "query" {
    type        = "string"
    required    = true
    description = "Search query (e.g. 'language:rust stars:>100')"
  }

  param "per_page" {
    type        = "integer"
    required    = false
    default     = 20
    description = "Results per page (max 100)"
  }

  operation {
    protocol = "http"
    method   = "GET"
    url      = "{{ vars.base_url }}/search/repositories"

    auth {
      kind   = "bearer"
      secret = "github.token"
    }

    query = {
      q        = "{{ args.query }}"
      per_page = "{{ args.per_page }}"
    }
  }

  result {
    decode = "json"
    output = "Found {{ result.total_count }} repos:\n{% for r in result.items[:5] %}  - {{ r.full_name }}\n{% endfor %}"
  }
}
```

Run against staging:

```bash
earl call --env staging github.search_repos --query "language:rust"
```

For a field-by-field reference, see [Template Schema](/docs/template-schema). For using external secret URIs in environment-specific auth, see [External Secrets](/docs/external-secrets).