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:

KindLevel
production4
staging3
development2
sandbox1

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.

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). When an environment binds two connections of the same connector type and you don’t qualify, the run fails with a structured ambiguousBinding error listing the aliases.

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.