Design direction
Execution Topology
The evolving execution-topology model for tasks, services, networks, readiness, and dependency isolation in ota.
Recommended next
Current problem
Use this page when the repo has more than one execution plane and the current backend model stops being honest.
The current repo-wide backend choice is too coarse for repos where host orchestration and container workloads both matter.
- ota knows where tasks run, but not where services are controlled from
- ota does not yet model where services are reachable from
- container-backed repos can accidentally reuse host-native dependency trees like
node_modules, which leaks platform-specific binaries into the wrong runtime - repo-wide requirements flatten host control-plane tools and workload-plane tools together
- container workloads and host-managed services can therefore describe a truthful repo and still fail as a topology
The proposed model
- execution contexts define where workloads run
- execution-context support can be scoped explicitly with
only_onandonly_archso unsupported hosts block early and honestly - typed service managers define how services are controlled
- task-scoped workload listeners define how long-running app processes publish host-visible endpoints
- context-scoped endpoints define how services are reached from each context
- execution-context attachments can keep platform-sensitive dependency paths isolated from the host tree while the source checkout remains bind-mounted
- context-scoped readiness defines where probes run from
- context-scoped requirements keep host and workload dependencies separate
Execution contexts
Contexts are the core boundary. They replace the one-flat-backend story with named workload planes.
Requirements declared on a context are inherited by every task that runs there, so keep the context narrow enough that those requirements stay truthful for all bound tasks.
execution: default_context: app contexts: host: backend: native requirements: tools: docker: "*" lsof: "*" host-dev: extends: host requirements: runtimes: python: ">=3.12,<3.14" node: "^22.12.0" tools: poetry: ">=1.8" npm: ">=10.5.0" app: backend: container lifecycle: persistent container: image: maven:3.9.14-eclipse-temurin-21-noble attachments: compose: - local requirements: runtimes: java: ">=21" node: ">=24.14.1" tools: maven: "*"tasks: setup:config-basic: context: host run: make setup-config-basic build: context: host-dev run: make build test:frontend: context: host-dev run: make testTasks, workloads, and services
Tasks bind to contexts. Services use typed managers. Workload listeners keep app ingress on the task and publish host-visible endpoints without overloading dependency services.
tasks: compose:up: context: host run: docker compose up -d postgres setup: internal: true context: app run: mvn -q -DskipTests dependency:go-offline db:integration: context: app requires_services: - postgres run: mvn -B -Dgroups=db-integration test dev: context: app requires_services: - postgres run: pnpm 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: / services: postgres: manager: kind: compose name: local file: compose.yaml 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 5432Container-local dependency isolation
Use this when source must stay bind-mounted but platform-sensitive install artifacts should live in Ota-managed container storage instead of the host checkout.
- use
attachments.isolated_pathsfor dependency trees likenode_moduleswhen native binaries need to match the container platform - the repo source stays bind-mounted, but Ota overlays engine-managed named volumes for isolated paths so host and container artifacts stop colliding
- run install and dev tasks against the same isolated path so later container runs reuse the right platform-specific artifacts
ota cleanremoves current and drifted Ota-managed isolated dependency volumes for contexts that declareattachments.isolated_paths, including ephemeral contexts- cleanup ownership is label-backed (
dev.ota.managed, dependency-isolation kind labels, and repo token) so repos sharing oneproject.namedo not delete each other's state - when ownership cannot be proven, ota reports managed state as skipped instead of deleting it unsafely
execution: default_context: app contexts: app: backend: container lifecycle: persistent container: image: node:24-bookworm attachments: isolated_paths: - node_modules requirements: runtimes: node: ">=24.14.1" tools: pnpm: "*"tasks: setup: internal: true context: app run: pnpm install dev: context: app run: pnpm devListener mode rules
- use listener shorthand for common local HTTP or TCP listeners when the bind and projected host endpoint are the same fixed loopback port
- use the full listener form when you need custom bind address, container host publication, auto host ports, primary listener selection, non-default paths, or multiple projected endpoints
bind.port.mode: fixedmeans the workload must listen on one explicit internal port; use it for containerized apps and any workload whose ingress should stay stablebind.port.mode: discovermeans ota discovers the final listening port after a native task starts; use it for host-native dev servers that may auto-bump to a free portproject.host.port.mode: fixedmeans the published host URL should stay on one explicit port; use it when you want a stable bookmark, proxy target, or docs link- when
project.host.port.mode: fixedis selected on the projected primary listener, operators can pick a one-run public URL withota run <task> --host-port <port>without changing the internal bind port execution.contexts.<name>.container.resources.memory.minimum/defaultkeeps container memory truth on the execution context; useota run <task> --memory <size>for one-run container overrides--memoryis container-only and is rejected when requested below a declaredresources.memory.minimumproject.host.port.mode: automeans ota injects runtime URL env values before process start and reports the same resolved URL in receipts and summaries; ephemeral container runs pre-reserve a free host port, while persistent runs resolve the currently published host mapping- if more than one listener is projected to host, mark exactly one projected listener as
project.host.primary: trueso ota can choose one deterministic primary URL - native tasks may use
fixedordiscover, butdiscovermust not be paired with a fixed host projection - container tasks with
project.hostmust use a fixed internal bind port; only the host-side port may beauto - loopback-only container binds such as
127.0.0.1orlocalhostare invalid when the listener is projected back tohost
Listener shorthand
Use listener shorthand for the common local case where a task exposes one fixed HTTP or TCP port on loopback and the host-visible endpoint uses the same port.
The shorthand is authoring sugar. Ota normalizes it into the same listener model used by the full protocol, bind, and project.host form.
- do not mix shorthand keys with verbose listener fields on the same listener
- do not define both
httpandtcpon one listener - switch to the full listener form as soon as bind and host projection differ
- switch to the Surfaces guide when the same endpoint is reused across multiple tasks, workflow readiness, or workflow exposes
Common local HTTP listener
Use this when a dev server, app server, or docs preview exposes one fixed local HTTP port and the browser should open the same loopback port.
web.http: 3000 means listener web, protocol http, fixed bind port 3000, fixed host projection 127.0.0.1:3000, and host path /.
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 app exposes a raw TCP listener and an open socket is the right readiness truth.
redis.tcp: 6379 means listener redis, protocol tcp, fixed bind port 6379, and fixed host projection 127.0.0.1:6379 with no HTTP path.
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 shape Ota normalizes the HTTP shorthand into.
Use the expanded form directly when the task needs custom bind address, container host publication, auto host ports, primary listener selection, or a non-default path.
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: /Workload endpoint projection contract
Use this when one long-running task needs one truthful host URL without making developers guess host ports.
- keep the app command simple (
run: pnpm dev) and let ota own host projection - fixed bind means the app always listens on one internal port inside the container
- auto host projection means ota picks a free host port so stale containers and port collisions do not force manual edits
- if the primary projected listener uses
project.host.port.mode: fixed,ota run <task> --host-port <port>can override only the published host/public port for one run - with multiple projected listeners,
project.host.primary: truedeclares the one listener that controlsOTA_PUBLIC_URLand summary endpoint rendering - ota exports
OTA_PUBLIC_URL,OTA_PUBLIC_HOST,OTA_PUBLIC_PORT, andOTA_PUBLIC_URL_<LISTENER>before task start when the projection is known - use
OTA_PUBLIC_URLwhen the app needs one canonical public URL, and useOTA_PUBLIC_URL_<LISTENER>for secondary listener-specific links
tasks: dev: context: app run: pnpm 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 primary: true port: mode: auto path: / metrics: protocol: http bind: address: 0.0.0.0 port: mode: fixed value: 9090 project: host: address: 127.0.0.1 port: mode: auto path: /metricsClone, run, open flow
- ota owns host-port allocation and prints one reachable URL
- ota keeps container memory requests explicit at execution-context scope and lets operators override per run when needed
- the URL in terminal output, receipts, and JSON stays aligned with what was injected into the task env
ota run <task> --logwrites durable task stdout/stderr artifacts under.ota/state/logs/; ota still keeps its own interpretation in the run summary and receipt, which also report that log location- readiness remains separate: service checks answer
is postgres up?, listener projection answershow do I reach the app?
git clone <repo>cd <repo>ota run dev# optional one-run fixed host override (for fixed projected listeners)ota run dev --host-port 4000# optional one-run container memory overrideota run dev --memory 4GiB# optional durable stdout/stderr capture for postmortem debuggingota run dev --log# open the URL ota prints (same value as OTA_PUBLIC_URL)Ingress troubleshooting
declares multiple projected listeners ... but none sets project.host.primary: true: mark exactly one projected listener as primarydeclares multiple listeners with project.host.primary: true: keep one primary listener and remove the restcannot project to the host from a loopback-only container bind address: usebind.address: 0.0.0.0for projected container listenerscould not publish host port: keepproject.host.port.mode: auto, stop stale containers occupying the same address/port, and rerun--host-portrejected: use it only for projected listeners withproject.host.port.mode: fixed; keepautolisteners on automatic reservation
Two concrete topologies
Topology
Container App + Compose Postgres
The app runs in a container context, joins the Compose network, reaches Postgres by service name, and lets tasks declare that dependency with requires_services.
Use this when the database is another Compose-managed container and the workload should stay inside the app image.
Open topology example →Topology
Container App + Host Postgres
The app still runs in a container context, but the database is host-managed and exposed through an app-context endpoint instead of a Compose network.
Use this when the workload is containerized but the database lives on the host machine.
Open topology example →What changes in doctor, up, and run
Use requires_services when a task should own its service prerequisites instead of depending on a manual ota up reminder.
ota keeps service ownership in services, starts what is needed before the task body runs, and keeps workload ingress with the task that owns the app process.
ota doctorvalidates context requirements, manager availability, endpoint projection, and readiness from the declared contextota doctorrejects impossible workload listener projection plans such as loopback-only container binds with host publicationota doctor,ota explain, andota receiptnow fail clearly when multi-listener projection omits a primary listener or marks more than one listener as primary- when a declared readiness context cannot actually execute,
ota doctornow reports that as an explicit topology blocker instead of pretending the service itself simply failed readiness - when a container context declares
attachments.isolated_paths,ota doctorandota explainsurface the isolated dependency paths instead of hiding them behind generic container execution notes - when a repo depends on remote execution contexts, native doctor mode reports local checks as a partial view instead of treating host checks as remote truth
ota doctor --mode remoteprobes runtime/tool versions, policy-backed provisioning installability, and approved org-policy version surfaces through executable remote contextsota upstarts required services from the control plane, ensures attachments are valid, runssetupin its task context, then re-checks readiness from the correct contextota uponly reports workload endpoints for runtime-bearing tasks it actually executes during preparation today, so it stays honest about which app paths were really startedtasks.<name>.requires_serviceslets a task request canonical services before its body runs, soota run db:integrationcan bring uppostgresand verify it automatically without turning the service into a task dependency- if
setupresolves to a remote context,ota upnow blocks explicitly instead of silently falling back to native diagnosis and pretending the host view was authoritative ota runresolves the task context first, attaches workload containers to declared service topology when required, injects resolved runtime URL env values before spawn when available, and records the same resolved workload endpoint for receipts/JSONota runresolves container memory from context resources (minimum/default) and applies--memoryoverrides directly to engine invocation for ephemeral and reconciled persistent runsota run <task> --logpreserves durable task stdout/stderr artifacts under.ota/state/logs/so failed or cleaned-up runs still leave postmortem evidence, while ota's own interpretation stays in receipt text, run summary output, and receipt JSON alongside the same log pathota runnow supports one-task mode-aware execution:tasks.<name>.execution.default_modepicks the default execution plane andota run <task> --mode <mode>selects any matching mode branch forcontext,lifecycle,env, command body, and runtime listeners- when a task has no matching mode branch,
ota runfalls back to the task-level execution body and task-level execution settings instead of requiring redundant empty branches - for container contexts with
attachments.isolated_paths,ota runandota upmount Ota-managed dependency-isolation named volumes before process start so platform-specific artifacts stay out of the host tree - for container
project.host.port.mode: auto, ephemeral runs verify the engine published the reserved host port and retry bounded times on host-port conflicts before failing clearly, while persistent runs verify the current published mapping - receipts and JSON output should report the same resolved topology truth instead of one generic repo backend
Status and migration
- the execution-context foundation is shipped:
execution.default_context,execution.contexts,contexts.<name>.requirements,tasks.<name>.context, andtasks.<name>.requires_servicesare accepted by the contract and now driveota run,ota up, and context-aware readiness diagnosis - mode-aware task execution is shipped:
tasks.<name>.execution.default_modeandtasks.<name>.execution.modes.<mode>let one task intent support native/container/remote execution shapes without duplicating task names - container dependency isolation is shipped:
execution.contexts.<name>.attachments.isolated_pathsnow mounts Ota-managed, engine-owned named volumes for platform-sensitive paths likenode_modulesin container contexts - task-scoped workload listeners are shipped:
tasks.<name>.runtime.kind: serviceplus named listeners now drive runtime endpoint resolution inota run, receipts, and the tasksota upactually executes during preparation - typed compose and host service managers are shipped for explicit control-plane ownership instead of legacy inferred service surfaces
- context-scoped
services.<name>.endpointsandservices.<name>.readiness.fromare now shipped and used byota doctorplusota services - container contexts with
attachments.composenow attach workload containers to the derived Compose network duringota runandota uptask execution - current
execution.preferredandservices.provider/start/stop/healthcheckstay as legacy compatibility mode - legacy service fields remain host-bound unless upgraded to typed topology-aware service blocks
- broader topology work is still in progress, including richer manager kinds beyond compose/host plus deeper per-context remote policy reporting and broader remote topology coverage