Reference

Environment Variables

How ota turns declared env requirements into deterministic, reviewable runtime values.

referenceautomation buildersintermediatestable2026-05-30

What this page should answer

Use this page when a repo needs environment variables and you want to know what ota will actually run with.

The goal is simple: know where to declare a value, know who wins, and know how to prove the winner before a task surprises you.

The named examples in this page, like DATABASE_URL, JAVA_HOME, AWS_PROFILE, and PATH, are examples only. The rules apply to any env variable the contract declares.

  • the repo contract declares what the repo needs in env.vars
  • ota resolves one winner per declared env var in a deterministic order
  • ota injects that winning value into the process it starts through ota run or ota up
  • policy can satisfy declared requirements, but policy does not invent new repo requirements on its own
  • declared env sources are explicit input, not hidden magic

Where each value belongs

Most confusion comes from putting the same value in the wrong place.

Use the contract to say what the repo needs, then use task env, declared sources, or policy only for the specific boundary they own.

  • put repo requirements in env.vars
  • use env.sources when the repo intentionally depends on declared source files such as dotenv, properties, json, yaml, or toml
  • use tasks.<name>.requirements.env when one task or workflow path needs a declared env var to become required without turning it into repo-global truth
  • use tasks.<name>.env when one task needs a fixed override
  • use tasks.<name>.env_bindings.<KEY>.from_service when one task needs an env value derived from a declared service endpoint instead of a hard-coded host alias
  • use .ota/org-policy.yaml policies.env.values when the org wants an approved shared value
  • workspace policy values live in ota.workspace.yaml and sit above org policy for workspace commands

Winner order

This is the shortest useful mental model for env resolution.

If a variable looks wrong at runtime, read this list from the top down and stop at the first place that sets it.

  • Repo commands: 1. task env, 2. org policy env, 3. process env, 4. declared env sources in order, 5. contract default
  • Workspace commands: 1. task env, 2. workspace policy env, 3. org policy env, 4. process env, 5. declared env sources in order, 6. contract default
  • Earlier declared source entries win over later ones because env.sources is ordered.
  • A contract default only wins when every higher-precedence layer is absent for that variable.
  • Required values with no match fail validation or execution instead of silently disappearing.

Why root env matters

Root contract env is not only a validation surface. It is the repo-wide execution contract ota uses when it starts work for the repo.

  • it tells ota which env names belong to repo readiness and execution truth
  • it lets ota resolve those names from task env, policy env, the shell, declared sources, and defaults in one deterministic order
  • it lets ota enforce required, allowed, secret, and PATH composition before the task starts
  • without root env, ota still inherits whatever the shell already has, but ota cannot treat those names as part of repo truth with provenance, allowed-value checks, or declared source support
  • when ota starts a process through ota run or ota up, it injects the resolved winning values into that spawned process environment
  • this is how Node sees values in process.env, and the same applies to Python, Go, Java, Rust, shell scripts, and other stacks launched by ota
  • PATH is the one special env name ota can compose with prepend and append before launching the process
  • ota does not rewrite source code, create a .env file, or permanently mutate the user's shell session
  • the resolved env applies to the process ota starts, not to unrelated processes outside ota
  • if a repo standardizes on ota run or ota up, ota can functionally replace in-app dotenv loading for many apps; if developers still start the app directly outside ota, shell exports or the app's own dotenv loader may still matter

What env.sources means

env.sources makes file-backed env loading explicit instead of magical.

A source entry describes the file ota may read and whether that file itself is part of readiness.

  • kind is curated, not open-ended; ota ships dotenv, properties, json, yaml, and toml
  • path is relative to the contract directory
  • must_exist: true means the file itself is part of readiness
  • a malformed declared source file is an error, not something ota silently skips
  • detector-led init only auto-suggests the current standard source paths it knows how to justify; runtime never auto-discovers undeclared files

Execution env layers

Root env.vars decides which repo-owned values are real. Execution env layers decide what else ota injects around those values for one task run.

  • execution.contexts.<name>.env sets context-wide execution defaults
  • tasks.<name>.env overrides those defaults for one task
  • tasks.<name>.execution.modes.<mode>.env overrides the selected mode branch
  • tasks.<name>.env_bindings and mode-specific env_bindings derive execution env from declared service endpoints, so native and container callers can use the same service truth without hard-coding backend-specific DNS names
  • from_service.password_env is the secret-safe service URL credential path; literal from_service.password is for disposable local/dev credentials only
  • ota injects OTA_WORKSPACE automatically, plus OTA_HOST_WORKSPACE for the real host-side repo root, OTA_HOST_HOME for host-owned clone or cache roots, and OTA_HOST_UID / OTA_HOST_GID on Unix-like hosts when a task truthfully needs the host user/group ids for deterministic native or compose interpolation
  • ota can also derive fallback cache env for known attachment pairs such as .m2, .npm, .pnpm-store, .gradle, .pip-cache, and .pypoetry-cache
  • use ota env --task <name> when you need to see both the contract env winner and the effective execution env for one task
  • when the selected task or workflow closure references tasks.<name>.requirements.env, ota shows those declared names as required for that selected path even if the root contract keeps them optional repo-wide

Env variable rules

These fields control how ota treats one declared env variable after it resolves a candidate value.

  • required means the env var must resolve or ota fails readiness or execution
  • tasks.<name>.requirements.env can make a declared env var required only for one selected task/workflow path without changing its repo-global requiredness
  • secret means ota treats the value as sensitive and redacts it in output and receipts
  • default means ota uses that value only when no higher-precedence layer wins
  • allowed means the winning value must stay inside a fixed set even if it came from policy, the shell, or a declared source file
  • DISCORD_TOKEN must resolve and ota redacts it in output
  • RELEASE_CHANNEL falls back to stable only when nothing higher-precedence wins
  • RELEASE_CHANNEL=beta is rejected even if it came from policy, the shell, or a declared source file
Rulesyaml
env:  vars:    DISCORD_TOKEN:      required: true      secret: true    RELEASE_CHANNEL:      default: stable      allowed:        - stable        - canary
Validyaml
env:  vars:    DISCORD_CHANNEL:      default: general
Invalidyaml
env:  vars:    DISCORD_CHANNEL: general

Remote secret caveat

Secret env values have one extra boundary beyond ordinary env resolution.

  • ota redacts secret: true values in output and receipts
  • remote shell execution rejects secret env values instead of inlining them into remote command strings
  • native and container execution still receive the resolved secret value in the spawned process environment

PATH is special

PATH is an ordered search path, so ota treats it differently from a plain value like JAVA_HOME.

  • use PATH when order matters and repo-local or toolchain bin directories should win
  • use prepend when entries should come first
  • use append when entries should be fallback locations
  • ota composes PATH around the chosen base value

Examples

These examples are not meant as a wall of YAML. Each one answers one specific authoring question.

Read them in order when you are learning the model, or jump to the one that matches the boundary you are trying to model.

  • start with repo truth in env.vars and declared files in env.sources
  • move to env_bindings when the value should follow a declared service endpoint instead of a hard-coded host alias
  • use policy examples only when the org or workspace really owns the winning value
  • use ota env to prove the winner after authoring instead of assuming the precedence path worked

Repo Truth

Use this shape when the repo needs a normal contract-owned env model: required secrets, safe defaults, and one or more declared dotenv files.

This is the baseline example because most repos should start here before they add task overrides, service-derived env, or policy-owned values.

  • DISCORD_TOKEN is a required secret, so ota enforces presence and redacts output
  • CRON_TIMEZONE is a normal defaulted value, so the contract can own a stable fallback
  • PATH.prepend is shown here because path lookup order is part of execution truth, not shell folklore
  • declaring both .env.local and .env keeps the file boundary explicit instead of asking ota to guess which local file matters
Repo Contractyaml
env:  vars:    DISCORD_TOKEN:      required: true      secret: true    SUPABASE_URL:      required: true    CRON_TIMEZONE:      default: Africa/Lagos    PATH:      prepend:        - ./node_modules/.bin  sources:    - kind: dotenv      path: .env.local    - kind: dotenv      path: .env      must_exist: true tasks:  test:    env:      CI: "true"    command:      exe: pnpm      args: [test]

Service Bindings

Use env_bindings when a task should follow service truth instead of freezing network addresses into env strings.

This is the stronger shape for database and queue URLs because native and container execution can project different reachable endpoints from the same declared service.

  • DB_HOST is derived from the declared postgres endpoint, so the task does not hard-code 127.0.0.1, localhost, or host.docker.internal in repo truth
  • password_env is the secret-safe path because the credential still comes from declared env truth instead of being embedded into the contract
  • this keeps endpoint projection, secret handling, and task env ownership aligned to one model
Service URLyaml
env:  vars:    DATABASE_URL:      secret: true    POSTGRES_PASSWORD:      secret: trueexecution:  contexts:    host:      backend: native services:  postgres:    endpoints:      host:        address: 127.0.0.1        port: 5432 tasks:  test:    requires_services:      - postgres    env_bindings:      DB_HOST:        from_service:          service: postgres          view: host          format: host      DATABASE_URL:        from_service:          service: postgres          view: host          format: url          scheme: postgres          username: postgres          password_env: POSTGRES_PASSWORD          database: app_test    run: bundle exec rails test
Dev-only Passwordyaml
env:  vars:    DATABASE_URL:      secret: trueexecution:  contexts:    host:      backend: nativeservices:  postgres:    endpoints:      host:        address: 127.0.0.1        port: 5432 tasks:  test:    env_bindings:      DATABASE_URL:        from_service:          service: postgres          format: url          scheme: postgres          username: postgres          password: postgres          database: app_test

Policy Values

Use policy when the winning value belongs to the organization or workspace, not because you want to skip declaring the env var in the repo contract.

The repo must still declare the requirement in env.vars; policy only supplies the approved winner.

  • DOCS_SITE_BASE_URL is still a repo requirement even when policy supplies the resolved value
  • workspace policy wins above org policy for workspace commands, so a workspace can intentionally narrow or override the shared org default
  • RELEASE_CHANNEL.allowed still constrains the final winner even when policy supplies it
Org Policyyaml
policies:  env:    values:      DOCS_SITE_BASE_URL: https://docs.internal.example      RELEASE_CHANNEL: stable
Workspace Policyyaml
# ota.workspace.yamlpolicies:  env:    values:      RELEASE_CHANNEL: canary      DOCS_SITE_BASE_URL: https://workspace.internal.example
Repo Contractyaml
# repo ota.yamlenv:  vars:    DOCS_SITE_BASE_URL:      required: true    RELEASE_CHANNEL:      default: stable      allowed:        - stable        - canary  sources:    - kind: dotenv      path: .env.local

Source vs Default

Use a contract default only as a fallback.

This example shows one winner: when a declared source provides the value, that source should win and the page should make that precedence visible instead of leaving it implied.

  • this example is useful when a team keeps one stable fallback in the contract but still wants .env.local to override it locally
  • the value still has to satisfy allowed, so source precedence does not bypass contract constraints
Source Overrideyaml
env:  vars:    RELEASE_CHANNEL:      default: stable      allowed:        - stable        - canary  sources:    - kind: dotenv      path: .env.local # .env.localRELEASE_CHANNEL=canary

Prove the Winner

Use ota env when you want evidence, not intuition.

This is the operator-facing proof lane for env resolution: which files loaded, which source won, which values were redacted, and which task-local overrides apply.

  • this is the step that turns env modeling from YAML authoring into reviewable runtime truth
  • use it before changing shell exports or source files when something looks wrong
  • task-scoped execution env stays separate from contract env, so CI=true shows up as task-owned instead of being confused with repo-global truth
ota env Outputtext
ENV ./ota.yaml Ready: yes Declared env sources- dotenv .env.local status=loaded- dotenv .env must_exist=true status=loaded Contract env- DISCORD_TOKEN required=true value=*** source=dotenv:.env status=resolved- DOCS_SITE_BASE_URL required=true value=https://docs.internal.example source=org policy status=resolved- RELEASE_CHANNEL required=false value=stable source=default status=resolved allowed=[stable, canary] Task env- CI value=true source=task status=task

PATH Composition

Use this when executable lookup order is part of the repo's real execution model.

This matters when repo-local bins, virtual environments, or toolchain-owned shims must win deterministically instead of relying on whatever shell order a developer already has.

  • showing the before-and-after path is useful because PATH is the one env key ota composes rather than replaces blindly
  • this is the right shape when order matters; use plain env values for non-path variables like JAVA_HOME
PATH Outputtext
If the process PATH is:/usr/local/bin:/usr/bin:/bin and the contract says:env:  vars:    PATH:      prepend:        - ./node_modules/.bin the final PATH is:./node_modules/.bin:/usr/local/bin:/usr/bin:/bin

How to use it

If you want env behavior to stay reviewable instead of tribal knowledge, follow this sequence.

  • declare env requirements in env.vars first so the repo contract stays the source of truth
  • declare source files in env.sources only when the repo intentionally depends on them
  • use tasks.<name>.requirements.env when a runtime, release, or selective workflow path needs one declared env var to become required only on that path
  • use task env when a single task needs a fixed override such as CI=true
  • add a policy layer only when your org wants approved values or shared defaults
  • use default when the repo has a safe fallback and allowed when the value must stay inside a fixed set
  • use must_exist: true only when the file itself is part of readiness, not just a convenient source
  • use root env when the repo needs env values to stay visible and enforceable at execution time, not just when you want validation
  • use doctor to find missing, invalid, or overridden env before running the task
  • use ota env when you need to prove which source won for each declared env var
  • use run or up to confirm the resolved env actually works in the real execution path
  • inspect the execution receipt or JSON output when you need provenance after execution

Where it shows up

  • doctor diagnoses missing or invalid env values and missing or broken declared sources
  • ota env shows both declared source status and the winning source for each env var
  • run and up consume env values during execution
  • diff can show env requirement impact before you write the contract
  • explain can turn env failures into a fix plan
  • execution receipts should record which env source won
  • workspace doctor should preserve root/member provenance instead of flattening it

What policy is not

  • not a replacement for env.sources
  • not a way to invent new env requirements the repo contract never declared
  • not a general application config system
  • not silent env mutation
  • not hosted control-plane workflow logic
  • not waiver or approval orchestration
  • not fleet reporting or retention policy