Skip to content

Environments

An environment is a named set of aliases pointing at connections. A flow asks for an alias; the environment decides which real connection that resolves to today.

Environments live under config/environments/{name}.environment.yaml:

Terminal window
kind: production
description: "Production environment for the nightly archive pipeline."
connections:
sftp:
connection: archive-sftp
http:
connection: zenvara-default

Three fields:

  • kind: — required guard tier: production, staging, development, or sandbox. It drives real behaviour — cross-environment validation (see below), kind-aware authorization, and the block on ephemeral inline runs. It is not a copy of your infrastructure; it is the safety level this environment runs at.
  • extends: — optional list of parent environments composed in order; the child can override individual aliases. Single-level only — no transitive chains.
  • connections: — the alias map. Each entry binds a short alias (the name a flow uses) to a named connection through a nested connection: field. An optional per-alias with: block patches that connection’s fields for this environment only.

A flow references the alias, never the connection name directly:

Terminal window
using:
- environment/prod
- zenvara/sftp
output:
ok: !bool
steps:
- $deploy:
create: sftp.file
on: sftp # the alias from the environment
with:
Path: "/htdocs/index.html"
Input: "${build.dist-path}/index.html"
- return:
ok: true

Same flow, different environments — dev maps sftp to a sandbox SFTP, prod maps it to the live one. (on: replaced the older connection: keyword.)

A single product-sync flow runs against dev, staging, and prod without YAML edits. Three environment files, one flow, three behaviours:

config/environments/dev.environment.yaml
kind: development
connections:
db:
connection: warehouse-dev
sftp:
connection: drop-zone-dev
# config/environments/staging.environment.yaml
kind: staging
connections:
db:
connection: warehouse-staging
sftp:
connection: drop-zone-staging
# config/environments/prod.environment.yaml
kind: production
connections:
db:
connection: warehouse-prod
sftp:
connection: drop-zone-prod
Terminal window
zen run-flow product-sync --env dev
zen run-flow product-sync --env staging
zen run-flow product-sync --env prod

The flow is the same file in storage; the environment chosen at run time routes it to a different set of services.

Validation is level-based, not symmetric: a higher-kind environment may use lower-kind connections (a production env can read from a staging connection — useful for read-only audit scrapes), but a lower-kind env cannot pull from a higher-kind one (a staging env asking for a production connection fails validation). Default levels:

Kind Level
production 4
staging 3
development 2
sandbox 1

This is a guard against the classic “ran dev flow against prod DB” mistake, and it is unconditional: kind: is a required, typed field on every connection (ZEN-2711) — a definition that omits it is rejected at parse time, not silently treated as untyped — and there is no config key that disables the cross-environment check.

Two things follow from the level table rather than from any opt-out. A connection declared at the lowest tier (kind: sandbox, level 1) can be bound into an environment of any kind, since its level is never higher than the environment’s. And a connection with kind: base — an extends-only template (see Inheritance & Worked Example) — is never bound directly into an environment at all, so it never reaches this validation.

Create a new environment when you have a new deployment target — a distinct set of real systems the same flows should run against. Sandbox versus live. A per-customer production estate. A read-only audit region. Each target is one environment.

Do not create a new environment to add a connection. If the only change is one more database or one more API, add it as a new alias in an existing environment’s connections: map. Counting environments by deployment target (not by connection) is what keeps the model small: three targets and a dozen connections is still three environments.

A connection is also the only licensing-counted unit — environments and branches are free structure on top — so there is never a cost reason to split one target into several environments. See Environments vs Branches for the full picture.

Why binding routes through the environment

Section titled “Why binding routes through the environment”

A flow — and a trigger — selects an environment, never a connection directly. That is deliberate (ZEN-1446):

  • Portability. A flow names the alias db, and the environment decides which connection that is. Binding a connection at run time would hard-wire the flow to one host and defeat the alias.
  • The guard tier stays enforceable. Routing through the environment is what lets kind cross-validation catch a dev run pointed at a production connection. A direct connection binding would bypass that guard.

If you genuinely need a fixed connection regardless of which environment runs, map that alias to the same connection in every environment — the alias stays portable, the target stays stable. To pin a single step or trigger to one declared environment, qualify it: on: <env>/<alias> (for a step) or on: <env> (for a trigger). A connector/entity invoke call is different: it names only an environment (on=<env> over MCP/REST), so if that environment binds two connections of the same connector type, the call fails with a structured ambiguous-binding error listing the aliases — qualifying with a specific alias is what resolves it. See Environments vs Branches for how the four on spellings differ.

A trigger can pin its run to an environment with on:, so one flow can fire against different targets on different schedules — an incremental sync against prod, a nightly full reload against test. A trigger’s on: must name an environment the flow declares in using: (else validation raises V1317). See From Dev to Prod for the worked pattern.