Reference
Environment Variables
How ota turns declared env requirements into deterministic, reviewable runtime values.
Recommended next
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 runorota 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.sourceswhen the repo intentionally depends on declared source files such as dotenv, properties, json, yaml, or toml - use
tasks.<name>.requirements.envwhen one task or workflow path needs a declared env var to become required without turning it into repo-global truth - use
tasks.<name>.envwhen one task needs a fixed override - use
tasks.<name>.env_bindings.<KEY>.from_servicewhen one task needs an env value derived from a declared service endpoint instead of a hard-coded host alias - use
.ota/org-policy.yamlpolicies.env.valueswhen the org wants an approved shared value - workspace policy values live in
ota.workspace.yamland 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.sourcesis ordered. - A contract
defaultonly 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, andPATHcomposition 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 runorota 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 PATHis the one special env name ota can compose withprependandappendbefore launching the process- ota does not rewrite source code, create a
.envfile, 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 runorota 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.
kindis curated, not open-ended; ota shipsdotenv,properties,json,yaml, andtomlpathis relative to the contract directorymust_exist: truemeans 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>.envsets context-wide execution defaultstasks.<name>.envoverrides those defaults for one tasktasks.<name>.execution.modes.<mode>.envoverrides the selected mode branchtasks.<name>.env_bindingsand mode-specificenv_bindingsderive execution env from declared service endpoints, so native and container callers can use the same service truth without hard-coding backend-specific DNS namesfrom_service.password_envis the secret-safe service URL credential path; literalfrom_service.passwordis for disposable local/dev credentials only- ota injects
OTA_WORKSPACEautomatically, plusOTA_HOST_WORKSPACEfor the real host-side repo root,OTA_HOST_HOMEfor host-owned clone or cache roots, andOTA_HOST_UID/OTA_HOST_GIDon 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.
requiredmeans the env var must resolve or ota fails readiness or executiontasks.<name>.requirements.envcan make a declared env var required only for one selected task/workflow path without changing its repo-global requirednesssecretmeans ota treats the value as sensitive and redacts it in output and receiptsdefaultmeans ota uses that value only when no higher-precedence layer winsallowedmeans the winning value must stay inside a fixed set even if it came from policy, the shell, or a declared source fileDISCORD_TOKENmust resolve and ota redacts it in outputRELEASE_CHANNELfalls back tostableonly when nothing higher-precedence winsRELEASE_CHANNEL=betais rejected even if it came from policy, the shell, or a declared source file
env: vars: DISCORD_TOKEN: required: true secret: true RELEASE_CHANNEL: default: stable allowed: - stable - canaryenv: vars: DISCORD_CHANNEL: default: generalenv: vars: DISCORD_CHANNEL: generalRemote secret caveat
Secret env values have one extra boundary beyond ordinary env resolution.
- ota redacts
secret: truevalues 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
PATHwhen order matters and repo-local or toolchain bin directories should win - use
prependwhen entries should come first - use
appendwhen entries should be fallback locations - ota composes
PATHaround 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.varsand declared files inenv.sources - move to
env_bindingswhen 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 envto 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_TOKENis a required secret, so ota enforces presence and redacts outputCRON_TIMEZONEis a normal defaulted value, so the contract can own a stable fallbackPATH.prependis shown here because path lookup order is part of execution truth, not shell folklore- declaring both
.env.localand.envkeeps the file boundary explicit instead of asking ota to guess which local file matters
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_HOSTis derived from the declaredpostgresendpoint, so the task does not hard-code127.0.0.1,localhost, orhost.docker.internalin repo truthpassword_envis 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
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 testenv: 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_testPolicy 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_URLis 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.allowedstill constrains the final winner even when policy supplies it
policies: env: values: DOCS_SITE_BASE_URL: https://docs.internal.example RELEASE_CHANNEL: stable# ota.workspace.yamlpolicies: env: values: RELEASE_CHANNEL: canary DOCS_SITE_BASE_URL: https://workspace.internal.example# repo ota.yamlenv: vars: DOCS_SITE_BASE_URL: required: true RELEASE_CHANNEL: default: stable allowed: - stable - canary sources: - kind: dotenv path: .env.localSource 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.localto override it locally - the value still has to satisfy
allowed, so source precedence does not bypass contract constraints
env: vars: RELEASE_CHANNEL: default: stable allowed: - stable - canary sources: - kind: dotenv path: .env.local # .env.localRELEASE_CHANNEL=canaryProve 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=trueshows up as task-owned instead of being confused with repo-global truth
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=taskPATH 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
PATHis 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
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:/binHow to use it
If you want env behavior to stay reviewable instead of tribal knowledge, follow this sequence.
- declare env requirements in
env.varsfirst so the repo contract stays the source of truth - declare source files in
env.sourcesonly when the repo intentionally depends on them - use
tasks.<name>.requirements.envwhen 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
defaultwhen the repo has a safe fallback andallowedwhen the value must stay inside a fixed set - use
must_exist: trueonly 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
doctorto find missing, invalid, or overridden env before running the task - use
ota envwhen you need to prove which source won for each declared env var - use
runorupto 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
doctordiagnoses missing or invalid env values and missing or broken declared sourcesota envshows both declared source status and the winning source for each env varrunandupconsume env values during executiondiffcan show env requirement impact before you write the contractexplaincan turn env failures into a fix plan- execution receipts should record which env source won
workspace doctorshould 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