Operate
Environment Model
How root env requirements, declared sources, policy values, and execution env layers fit together in one deterministic model.
Recommended next
Why this page exists
Environment handling in Ota is now more than one small field in the contract.
A repo can declare root env requirements, explicit file-backed sources, context-wide execution defaults, task-local overrides, mode-specific overrides, and Ota-owned injected values, all without losing determinism.
- root
env.varssays which values are part of repo truth - root
env.sourcessays which files ota may read values from - workspace policy and org policy can satisfy declared env requirements without inventing new ones
- execution contexts and tasks can still add execution-only env on top
- the winner is explainable through
ota env,ota doctor, receipts, and JSON output
The model in one view
env.varsis the requirement layer: required, secret, default, allowed, andPATHcompositionenv.vars.<NAME>.defaultlets the contract supply a non-secret fallback directly, so repos that standardize onota runorota upoften do not need a separate.envfile for simple defaultsenv.sourcesis the explicit file-loading layer: ota only reads declared source filesexecution.contexts.<name>.envis the context-wide execution default layertasks.<name>.envis the task override layertasks.<name>.execution.modes.<mode>.envis the selected mode override layertasks.<name>.env_bindingsis the service-derived execution layer for env values that must follow declared service endpoint truth across native and container pathsOTA_WORKSPACEand ota-derived cache env are injected execution helpers, not repo requirements
execution: default_context: app contexts: app: backend: container lifecycle: ephemeral env: NODE_ENV: development attachments: isolated_paths: - .npm env: vars: WEB_ORIGIN: default: "http://127.0.0.1:3000" API_BASE_URL: default: "http://127.0.0.1:8080" REQUEST_TIMEOUT_SECONDS: default: "15" DATABASE_URL: required: true secret: true RELEASE_CHANNEL: default: stable allowed: - stable - canary sources: - kind: dotenv path: .env.local - kind: properties path: config/app.properties tasks: build: env: CI: "true" command: exe: npm args: [run, build]Contract defaults can replace simple dotenv defaults
When a value is not secret and the repo should have one stable fallback, keep that truth in env.vars.<NAME>.default instead of scattering it across starter .env files.
This works especially well for app feature flags, local URLs, and timeout knobs when the repo is normally started through ota run or ota up.
- use contract defaults for non-secret values that should stay reviewable in
ota.yaml - keep secrets in policy, the shell, or declared
env.sources, not in contract defaults - app runtimes still receive the resolved values through the normal process environment, so Node and similar stacks do not need their own dotenv loader for those defaults
env: vars: WEB_ORIGIN: default: "http://127.0.0.1:3000" API_BASE_URL: default: "http://127.0.0.1:8080" REQUEST_TIMEOUT_SECONDS: default: "15" ANALYTICS_WRITE_KEY: required: true secret: true sources: - kind: dotenv path: .env.local tasks: dev: run: pnpm devWinner order
Root env resolution and execution env injection are related, but they are not the same layer.
- Repo commands resolve declared env vars in this order: task env, org policy env, process env, declared env sources in order, then contract default
- Workspace commands add one higher policy layer: task env, workspace policy env, org policy env, process env, declared env sources in order, then contract default
- Execution env injection then applies context env, task env, and selected mode env for the task that is actually running
- Explicit task or mode env still wins over Ota-derived fallback execution env
Declared source kinds
Source kinds are curated on purpose. Ota supports the common deterministic formats directly and rejects open-ended parser plugins or runtime auto-detection.
- shipped kinds are
dotenv,properties,json,yaml, andtoml pathis always relative to the contract directorymust_exist: truemakes the file itself part of readiness- nested structured sources flatten through
.before env-key normalization - arrays, object leaves, and
nullvalues are explicit errors - two keys that normalize to the same env name inside one source are a hard collision error
env: sources: - kind: dotenv path: .env.local - kind: properties path: src/main/resources/application.properties - kind: json path: appsettings.json - kind: yaml path: config/app.yaml - kind: toml path: config/env.tomlExecution-only env
Execution env exists so one plane can carry its own tool paths and runtime defaults without repeating the same values on every task.
- put shared plane defaults in
execution.contexts.<name>.env - put one-task overrides in
tasks.<name>.env - put mode-specific overrides in
tasks.<name>.execution.modes.<mode>.envonly when backend selection changes the correct value - put service-derived task env in
tasks.<name>.env_bindingswhen a value likeDB_HOSTorDATABASE_URLshould come from declaredservicesendpoints instead of hard-coded backend-specific hostnames - use
from_service.password_envfor real service URL credentials; literalfrom_service.passwordis only for disposable local/dev credentials - ota injects
OTA_WORKSPACEautomatically so container or remote workspace roots do not need to be hardcoded - ota can also derive fallback cache env for known attachment pairs such as
.m2,.npm,.pnpm-store,.gradle,.pip-cache, and.pypoetry-cache
execution: contexts: application: backend: container lifecycle: persistent env: JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 attachments: isolated_paths: - .m2 tasks: test: context: application env: MAVEN_OPTS: -Dmaven.repo.local=${OTA_WORKSPACE}/custom-m2/repository run: mvn testDetect versus runtime
Detect and init can help author the contract, but runtime stays explicit.
- detector-led init can infer curated
env.sourcesfrom known standard files such as.env.local,.env,src/main/resources/application.properties,src/main/resources/application.yml,src/main/resources/application.yaml,appsettings.json, andappsettings.Development.json - those inferred entries carry provenance and confidence like other detect/init output
- runtime commands do not auto-discover those files later
ota run,ota up,ota env, andota doctoronly load the source files that are already declared inenv.sources
How to use it
- start with
env.varswhen the repo needs one value to be real, reviewable, and enforceable - add
env.sourcesonly when the repo intentionally depends on source files for those values - use
tasks.<name>.requirements.envwhen one workflow path should require a declared env var without turning it into repo-global truth - use context env when one execution plane needs a shared default
- use task or mode env only for the smaller override boundary they actually own
- use
tasks.<name>.env_bindingswhen task env should be derived from declared service endpoints - declare both the generated env value and the referenced
password_envassecret: truewhen a service binding includes credentials - use
ota env --task <name>to prove the winner before a task surprises you - use
ota doctorwhen a source is missing, malformed, colliding, or structurally invalid