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.
Anatomy of an environment
Section titled “Anatomy of an environment”Environments live under config/environments/{name}.environment.yaml:
kind: productiondescription: "Production environment for the nightly archive pipeline."
connections: sftp: connection: archive-sftp http: connection: zenvara-defaultThree fields:
kind:— required guard tier:production,staging,development, orsandbox. 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 nestedconnection:field. An optional per-aliaswith:block patches that connection’s fields for this environment only.
How a flow uses an alias
Section titled “How a flow uses an alias”A flow references the alias, never the connection name directly:
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: trueSame flow, different environments — dev maps sftp to a sandbox SFTP, prod maps it to the live one. (on: replaced the older connection: keyword.)
One flow, many targets
Section titled “One flow, many targets”A single product-sync flow runs against dev, staging, and prod without YAML edits. Three environment files, one flow, three behaviours:
kind: developmentconnections: db: connection: warehouse-dev sftp: connection: drop-zone-dev
# config/environments/staging.environment.yamlkind: stagingconnections: db: connection: warehouse-staging sftp: connection: drop-zone-staging
# config/environments/prod.environment.yamlkind: productionconnections: db: connection: warehouse-prod sftp: connection: drop-zone-prodzen run-flow product-sync --env devzen run-flow product-sync --env stagingzen run-flow product-sync --env prodThe flow is the same file in storage; the environment chosen at run time routes it to a different set of services.
Cross-environment validation
Section titled “Cross-environment validation”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.
When to create a second environment
Section titled “When to create a second environment”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
kindcross-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.
Triggers select an environment too
Section titled “Triggers select an environment too”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.