Environments

Anything that changes between dev, staging, and prod (base URLs, keys, timeouts) lives in an environment, not in your requests. Requests reference variables with {{placeholders}}; environments supply the values at run time.

Declare, then fill

Variables are declared once in the environment schema, then given values per environment. Declaration comes first: a variable must exist in the schema before it can hold a value.

// apis/petstore-api/environments/schema.json
{
  "version": 1,
  "variables": {
    "baseUrl": { "description": "Base URL of the API", "type": "string", "required": true },
    "apiKey":  { "description": "API key", "type": "string", "required": true, "secret": true },
    "timeout": { "description": "Request timeout (ms)", "type": "number", "default": 5000 }
  }
}

The schema gives each variable a type, whether it's required, an optional default, and whether it's a secret.

You declare variables in the app's environment schema editor (there's no CLI for editing the schema). Once declared, set values with craftr env write or in the app.

Values and secrets

Non-secret values are committed; secrets are not. When you set a variable that the schema marks secret: true, API Craft routes it to a gitignored *.secrets.env file automatically; you don't manage two files by hand.

// apis/petstore-api/environments/dev.json   (committed)
{ "baseUrl": "https://petstore3.swagger.io/api/v3", "timeout": 10000 }
# apis/petstore-api/environments/dev.secrets.env   (gitignored)
apiKey=sk-dev-abc123xyz

Environments are discovered automatically from the *.json files in environments/ (except schema.json); there's no list to maintain.

Inheritance

An environment can extend another. Inheritance is recursive: each level overrides the one it extends, and secrets follow the same chain.

// prod.json
{ "baseUrl": "https://api.example.com" }
// prod-eu.json
{ "extends": "prod", "baseUrl": "https://api-eu.example.com" }
// prod-eu-client-a.json
{ "extends": "prod-eu" }

Resolving prod-eu-client-a walks prod → prod-eu → prod-eu-client-a, with each level overriding the previous. Cycles (a extends b extends a) are detected and rejected with a clear error.

extends, level, and tls are reserved keys; they're managed as inheritance, environment level, and TLS options, not as ordinary variables. The level (production, staging, or local; default local) is what triggers the app's confirmation prompt before you fire a request at a production environment.

TLS verification

By default, requests verify the server's TLS certificate. For an environment that points at a host with a self-signed or otherwise untrusted certificate (a local dev box, an internal staging server), you can turn verification off per environment:

// staging.json
{ "tls": { "verify": false } }

Verification is on unless verify is explicitly false. Like other reserved keys, tls is inherited along the extends chain (a child environment overrides its parent) and it's set from the app's environment panel, not the CLI. A run made with verification off records that fact in its result, so it's visible in history.

Disabling verification skips certificate checks for that environment's requests. Use it for trusted internal hosts, not for production traffic.

The environment panel showing the inheritance chain and variable values

How a placeholder is resolved

When a {{variable}} is resolved for a run, layers are merged in this order, later wins:

  1. Schema defaults: the default declared in the schema.
  2. Environment values and secrets: the merged extends chain; within an environment, secrets override plain values, and a child overrides its parent.
  3. System environment / --var: a process variable named API_CRAFT_VAR_<NAME> (uppercased). The CLI's --var name=value sets exactly this.

So --var and system environment variables win, then committed/secret values, then defaults. Only variables declared in the schema are read from the system environment, and any required variable that resolves to nothing fails the run with an explicit error.

# override a variable for a single run
craftr req run Pet/get-pet -e dev --var timeout=1000

Inside a workflow

Workflows add a layer of their own in front of the ones above. A {{token}} in a workflow is first matched against the run's own data: workflow inputs, earlier block results, transform outputs, and the current loop iterator. Whatever doesn't match is left alone and falls through to the environment layers above.

{ "body": { "name": "{{petName}}", "host": "{{baseUrl}}" } }

petName is a workflow input, so the workflow resolves it. baseUrl isn't, so it reaches the environment resolver, which uses the environment bound to that block's API, since a workflow can span several APIs and binds an environment per API at run time.

Only the environment layer is per-API. Inputs, block results, and transform outputs are global to the run, so a value produced against one API can feed a request to another.