Operate
Execution Contexts
Why contexts matter, what they change, and how they keep one repo honest across host and container execution.
Recommended next
Why contexts exist
Contexts tell Ota where a task actually runs.
That is one of the strongest parts of the product because it lets the same repo stay honest about host work, container work, service reachability, and published app URLs without relying on README drift or local conventions.
- a context chooses the execution plane: host, container, or remote
- a context can pin the toolchain, lifecycle, and backend-specific requirements for that plane
- a context can hold plane-wide env defaults so shared tool paths do not have to be repeated on every task
- a context changes what names and endpoints are reachable, such as
localhoston host versuspostgreson a Compose network - a context gives Ota a real boundary for readiness, task execution, and agent safety instead of making Ota infer it from shell commands
What a context changes
Where
Execution plane
The same task intent can mean host execution, container execution, or remote execution. Contexts make that explicit.
What
Toolchain and lifecycle
Contexts hold the runtime requirements, container image, lifecycle, attachments, and context-wide env defaults that belong to one execution plane.
How
Reachability and readiness
Contexts decide what a task can reach directly and which service endpoint shape is valid from that plane.
Default context versus task context
Use execution.default_context when most tasks belong to one execution plane.
Use tasks.<name>.context only when a task should intentionally break from that default.
- if almost every task runs in
app, setexecution.default_context: appand stop repeating it - keep per-task
contextonly when it changes meaning, such ascompose:upon host while builds and tests run inapp - this keeps contracts shorter and makes the exceptional tasks stand out
execution: default_context: app contexts: host: backend: native app: backend: container lifecycle: persistent container: image: node:24-bookworm tasks: setup: internal: true run: npm install test: run: npm test compose:up: context: host run: docker compose up -dWhen to use mode-aware branches
Sometimes one task intent should stay the same while the execution plane changes.
That is what task-level mode-aware execution is for. It lets ota run start stay one task while --mode native or --mode container selects the right branch honestly.
- Use one task when operators should remember one intent, like
start,build, ortest, while backend wiring changes under the hood. - Use mode branches when
context,env,lifecycle,run/script, orruntimechanges by backend and that change should be visible in contract review. - Use separate task names only when user-facing intent changes, such as
clean:startversusstart.
tasks: start: requires_services: - postgres execution: default_mode: container modes: native: context: host env: DB_URL: jdbc:postgresql://127.0.0.1:5432/app run: mvn spring-boot:run container: context: app lifecycle: persistent env: DB_URL: jdbc:postgresql://postgres:5432/app SERVER_ADDRESS: 0.0.0.0 SERVER_PORT: "8080" run: mvn spring-boot:runContexts, services, and app URLs
Contexts are where repo readiness stops being abstract.
A service can be ready from one context and unreachable from another. A workload can bind inside a container while Ota still publishes the correct host URL outside it.
- use
services.<name>.endpoints.<context>when the same service is reached differently from host and app planes - use
services.<name>.readiness.fromtogether with legacyrunor structuredkindwhen the readiness probe must run from a specific context - use
tasks.<name>.requires_serviceswhen a task only needs a service ready before it runs - use
tasks.<name>.runtime.listenerswhen the task itself is the workload that should publish a host endpoint
services: postgres: manager: kind: compose name: local file: docker-compose.yml service: postgres endpoints: host: address: 127.0.0.1 port: 5432 app: address: postgres port: 5432 readiness: from: app run: pg_isready -h postgres -p 5432 tasks: start: requires_services: - postgres execution: default_mode: container modes: container: context: app env: DB_URL: jdbc:postgresql://postgres:5432/app run: npm run dev runtime: kind: service listeners: http: protocol: http bind: address: 0.0.0.0 port: mode: fixed value: 3000 project: host: address: 127.0.0.1 port: mode: auto path: /Listener shorthand
Listener shorthand is the compact authoring path for common local app ports.
Use it when one task exposes one fixed HTTP or TCP port and the host-visible endpoint should use the same loopback port.
- shorthand and full listener fields are mutually exclusive on one listener
- a listener can declare one shorthand protocol, not both
httpandtcp - use the full listener form for container binds, auto host ports, primary listener selection, custom paths, or multiple projected endpoints
Common local HTTP listener
Use this for a local web app, API server, or docs preview that should be reachable at the same fixed loopback port it listens on.
web.http: 3000 expands to an HTTP listener on fixed port 3000 with host projection at http://127.0.0.1:3000/.
tasks: dev: run: npm run dev runtime: kind: service readiness: kind: http listener: web path: / listeners: web: http: 3000Common local TCP listener
Use this when an accepting socket is the correct readiness and reachability signal.
redis.tcp: 6379 expands to a TCP listener on fixed port 6379 with host projection at 127.0.0.1:6379.
tasks: redis: run: redis-server --port 6379 runtime: kind: service readiness: kind: tcp listener: redis listeners: redis: tcp: 6379Full HTTP listener form
This is the full listener shape behind the HTTP shorthand.
Use it directly when bind address, projected host address, host port mode, primary listener selection, or path behavior needs to be explicit.
listeners: web: protocol: http bind: address: 127.0.0.1 port: mode: fixed value: 3000 project: host: address: 127.0.0.1 port: mode: fixed value: 3000 path: /Task target bindings
Use tasks.<name>.targets.<target> when one task should point at either another repo-managed app by service identity or one explicit declared URL.
Keep the topology truth in targets. Use operator input only when the task genuinely needs an override channel.
targets.<name>is the consumer-local target name; keep it short and stable because ota uses it in receipts and, without an override input, exports it asOTA_TARGET_<TARGET>- choose exactly one identity shape per target:
servicefor repo-managed topology orurlfor one fixed declared URL - use
targets.<name>.service.taskto name the producer task that owns the service you want to follow; the value must be an existing service task in the same contract unless you also declareservice.memberfor a monorepo member producer orservice.repofor a workspace repo producer - use
targets.<name>.service.memberonly for monorepo member producers declared underworkspace.members; today that cross-member slice stays intentionally narrow:address_view: hoststaysactivation.mode: manualonly, whileaddress_view: topology/address_view: internalwork only when consumer and producer share one declared backend binding on the active plane and are the only member-target shapes that supportensure_started/restart_ready/ensure_running/ensure_ready - use
targets.<name>.service.repowhen the producer lives in another repo declared underota.workspace.yaml; the current shipped cross-repo slice stays explicit: onlyaddress_view: hostis supported, the producer listener must declare one fixedproject.hostendpoint, and host-view activation may reuse or start that producer through the owning repo contract before the consumer runs - use
targets.<name>.service.listenerto pick the exact producer listener to target; the value must be one of that producer's declared runtime listeners, but you may omit it when the producer exposes exactly one declared listener name - use
targets.<name>.service.address_viewto choose the reachable address shape:host,topology, orinternal - use
address_view: hostwhen the consumer may run in a different execution plane and should target the producer's published host URL - use
address_view: topologyonly when ota can truthfully resolve the current local topology address for that task shape - use
address_view: internalfor direct internal-plane addressing when caller and producer share one declared backend boundary on the active plane; unresolved cases still fail clearly today - use
override_inputonly when the operator may intentionally point the task at staging, preview, another local service, or another explicit URL; the value must name an input on the same task - use
activation.mode: manualwhen target resolution is enough,ensure_startedwhen ota should hand producer startup off immediately,restart_readywhen ota should bounce a reachable producer and verify it again,ensure_runningwhen ota should make the declared listener reachable first, andensure_readyonly when ota should also wait for a deeper declared readiness contract;urltargets stay manual - today non-manual activation is intentionally narrow: explicit overrides skip producer startup, compatibility defaults fail clearly, and actual producer auto-start is limited to producer shapes ota can own honestly: persistent container producer services, unix native producer services, built-in remote producer services only when caller and producer share one declared remote backend binding, and backend-provider remote producer services only when the matching
backend_providerextension declaresactivation.provider_managed_cleanup: true; built-in shared-remoteaddress_view: host/address_view: topology/address_view: internalmay probe the remote plane, and backend-provider remote activation now covers those same shared-remote views when ota can issue provider-owned cleanup first;ensure_startedhands startup off immediately,restart_readycan bounce a reachable producer before waiting again,ensure_runningobserves listener reachability, andensure_readymay observetcporhttpreadiness - when the producer service task declares
runtime.readiness, ota waits for that readiness contract instead of treating an open listener socket as sufficient - run summaries describe activation in plain terms:
started_started/reused_startedtalk about startup handoff,started_running/reused_runningtalk about listener reachability, andrestarted_ready/started_ready/reused_readytalk about deeper readiness - stream-mode runs show an explicit activation wait phase while ota is starting or waiting on the producer readiness contract
- if activation started the producer for this run, ota cleans it up on interrupt; reused producers are left running intentionally
- when
override_inputis present, ota resolves the target into that input unless the operator passed an explicit value - when
override_inputis omitted, ota exports the resolved URL asOTA_TARGET_<TARGET>so the task still gets a real runtime value
tasks: dev: run: pnpm dev:api runtime: kind: service listeners: http: protocol: http bind: address: 127.0.0.1 port: mode: fixed value: 8080 project: host: address: 127.0.0.1 primary: true port: mode: fixed value: 8080 path: / sandbox: inputs: base_url: description: Override the API base URL when needed targets: api: service: task: dev listener: http address_view: host override_input: base_url activation: mode: ensure_ready run: pnpm dev:sandbox probe:api: targets: api: service: task: dev listener: http address_view: host run: curl -fsS "$OTA_TARGET_API/health"What breaks without context
If the same repo mixes host tools and container workloads, missing context information creates subtle, environment-dependent failures.
The same task appears to pass in one terminal and fail in another because execution path and endpoint assumptions are hidden.
- A
localhostdatabase URL in one plane can succeed on host but fail in a container workload where onlypostgresDNS is available. - A containerized startup command that opens
127.0.0.1:3000may print an unreachable URL if the workload endpoint was never projected to a host-mapped port. - Service readiness checks may pass on host, but still fail during container work if probe context, manager control plane, and endpoints are not explicitly aligned.
tasks: dev: run: | ./gradlew bootRun # DB URL looks like localhost, but this depends on where the task runs. env: DB_URL: jdbc:postgresql://localhost:5432/app runtime: kind: service listeners: http: protocol: http bind: address: 127.0.0.1 port: mode: fixed value: 3000 project: host: address: 127.0.0.1 port: mode: fixed value: 3000execution: default_context: app contexts: host: backend: native app: backend: container lifecycle: persistent container: image: maven:3.9.14-eclipse-temurin-21-noble services: postgres: manager: kind: compose name: local file: docker-compose.yml service: postgres endpoints: host: address: 127.0.0.1 port: 5432 app: address: postgres port: 5432 readiness: from: app run: pg_isready -h postgres -p 5432 tasks: dev: context: app requires_services: - postgres env: DB_URL: jdbc:postgresql://postgres:5432/app runtime: kind: service listeners: http: protocol: http bind: address: 0.0.0.0 port: mode: fixed value: 3000 project: host: address: 127.0.0.1 port: mode: auto path: /Use this shape when the task should stay inspectable as a packaged command or packaged container runtime instead of collapsing back into shell.
tasks: quickstart: description: Start n8n through a packaged command launch: kind: command exe: npx args: [n8n] runtime: kind: service surfaces: - backend packaged: description: Start n8n through a packaged container image launch: kind: container image: docker.n8n.io/n8nio/n8n volumes: - name: n8n_data target: /home/node/.n8n runtime: kind: service surfaces: backend: bind: address: 0.0.0.0 port: mode: fixed value: 5678 project: host: address: 127.0.0.1 port: mode: fixed value: 5678 path: / primary: trueHost setup, container app
Keep Docker and Compose on the host while app work happens in a container context.
Use this when host orchestration and containerized app execution both need to stay explicit and truthful.
execution: default_context: app contexts: host: backend: native app: backend: container lifecycle: persistent container: image: node:24-bookworm tasks: compose:up: context: host run: docker compose up -d dev: context: app run: npm run devOne app, two truthful start paths
Keep one user intent (start) while behavior shifts by backend.
Use this when operators need stable task naming across host and container execution.
tasks: start: requires_services: - postgres execution: default_mode: container modes: native: context: host env: DB_URL: jdbc:postgresql://127.0.0.1:5432/app run: ./gradlew bootRun container: context: app lifecycle: persistent env: DB_URL: jdbc:postgresql://postgres:5432/app run: ./gradlew bootRunFull listener form: container bind, host URL
Keep container bind behavior stable while Ota publishes a host URL users can open.
Use the full listener form when a container workload needs deterministic internal networking, auto/fixed host projection, primary listener selection, or user-facing reachability that cannot be represented by shorthand.
tasks: dev: context: app lifecycle: persistent run: npm run dev runtime: kind: service listeners: http: protocol: http bind: address: 0.0.0.0 port: mode: fixed value: 3000 project: host: address: 127.0.0.1 port: mode: auto path: /Container memory, one-run override
Keep container memory policy in context and still allow one-run overrides.
Use this when local reliability depends on higher than default memory for a specific run.
execution: contexts: app: backend: container lifecycle: persistent container: image: node:24-bookworm resources: memory: minimum: 2GiB default: 2GiB tasks: test: context: app run: npm testDecision guide
- Set
execution.default_contextwhen one backend plane is the default operating mode for most tasks. - Use
execution.contexts.<name>.extendswhen two contexts share one base shape and only differ in a small override set. - Set per-task
contextonly when a task truly requires a different execution plane for correct behavior. - Choose mode-aware execution when one user intent must remain stable across more than one plane.
- Keep
clean:startseparate fromstartonly when the operation itself changes, not just where it runs. - When a mode cannot be resolved, fail clearly instead of silently choosing an alternate context.