Orientation

Topology decision guide

How to choose between repo-wide requirements, execution-context requirements, service managers, and task prerequisites.

learnnew usersintermediatestable2026-04-21

Start with the decision

Use the smallest contract scope that stays truthful.

If a dependency is shared everywhere, keep it at the repo level. If it only belongs to one execution plane, scope it to that context. If it belongs only to one front door, put it under tasks.<name>.requirements. If the dependency is host-native build tooling, define it under native_prerequisites and select it from the task that needs it. If Ota should own a service's lifecycle, put it under services. If a task only needs a service ready before it runs, declare that with tasks.<name>.requires_services.

Decision cheat sheettext
shared everywhere -> top-level runtimes/toolsone execution plane -> execution.contexts.<name>.requirementsone front door -> tasks.<name>.requirementshost-native build tooling -> native_prerequisites + requirements.nativeservice lifecycle -> services.<name>.managertask needs a ready service -> tasks.<name>.requires_services

Shared baseline vs execution context requirements

Use repo-wide requirements for dependencies that every execution path needs.

Use execution-context requirements when only one plane needs the runtime or tool, such as Docker on the host or Maven inside the app container.

Context requirements are inherited by every task bound to that context, so keep the context narrow enough that the dependency is truthful for all of those tasks.

Repo-wide

Top-level runtimes/tools

Use this for dependencies that every path needs, regardless of host, container, or remote execution.

When to use it

Choose this when the dependency is truly universal.

Plane-specific

Execution-context requirements

Use this for dependencies only one execution plane needs, such as Docker on host or Maven inside app.

When to use it

Choose this when every task in that execution context truly needs the dependency.

Scope exampleyaml
runtimes:  git: "*"tools:  jq: "*"execution:  contexts:    host:      backend: native      requirements:        tools:          docker: "*"    app:      backend: container      requirements:        runtimes:          java: ">=21"        tools:          maven: "*"

When the base context is too broad

If one host task needs only a shell and a file write, while the rest of the host-side workflow needs npm, Poetry, or other heavier tooling, do not put those tools on the broad host context.

Split the context instead. Keep host light, then add a narrower shared context such as host-dev for build, lint, test, and runtime tasks that really share the same dependency plane.

Use a narrower shared context instead of overloading `host`yaml
execution:  default_context: host  contexts:    host:      backend: native    host-dev:      extends: host      requirements:        runtimes:          python: ">=3.12,<3.14"          node: "^22.12.0"        tools:          poetry: ">=1.8"          npm: ">=10.5.0" tasks:  setup:config-basic:    context: host    run: make setup-config-basic   build:    context: host-dev    run: make build   lint:frontend:    context: host-dev    run: make lint-frontend

Contract baseline vs task-scoped requirements

Keep shared contract requirements at the repo root.

Keep contributor-only, quickstart-only, or Docker-only prerequisites on the task path that actually needs them.

Task-path requirements.tools are self-contained: tool names do not need duplicate top-level tools.<name> entries just to validate.

Use tasks.<name>.requirements.any_of when one task has explicit alternative prerequisite lanes (for example host-local vs docker-host) across runtimes, tools, toolchains, native, env, and checks.

Contract-wide

Top-level runtimes/tools

Use this for dependencies that every task, check, and execution path needs across the repo.

When to use it

Choose this when the dependency is a contract baseline, not a single service concern.

Task-scoped

Task requirements

Use this when only one front door needs a runtime, tool, env var, or precondition check.

When to use it

Choose this for repos with separate contributor, instant, and Docker paths.

Front-door prerequisite scopeyaml
toolchains:  node:    provider: corepack    version: "24" tools:  pnpm:    version: ">=10"    acquisition:      provider: corepack      package: pnpm      version: "10.22.0" native_prerequisites:  node-native-build-tools:    description: Native compiler toolchain for packages with native addons    platforms:      linux:        check: node-native-build-tools-linux        apt: [build-essential, python3]      macos:        check: node-native-build-tools-macos        xcode_clt: true      windows:        visual_studio:          components:            - Microsoft.VisualStudio.Component.VC.Tools.x86.x64        requires:          runtimes:            python: ">=3.10" checks:  - name: node-native-build-tools-linux    kind: precondition    severity: error    run: sh -c "cc --version && python3 --version"  - name: node-native-build-tools-macos    kind: precondition    severity: error    run: sh -c "xcode-select -p && python3 --version"  - name: workspace-dependencies-installed    kind: file    severity: error    path: node_modules    expect: directory tasks:  setup:env-local:    action:      kind: ensure_env_file      path: .env.local      template: .env.example      vars:        DATABASE_URL:          value: postgresql://localhost:5432/app        JWT_SECRET:          random:            bytes: 32            encoding: hex   dev:    run: pnpm dev    depends_on:      - setup:env-local    requirements:      runtimes:        node: ">=24"      tools:        pnpm: ">=10"      native:        - node-native-build-tools      checks:        - workspace-dependencies-installed   docker:run:    launch:      kind: container      image: ghcr.io/example/app:latest    requirements:      tools:        docker: "*"   setup:services:    context: host    run: bash scripts/setup-local-services.sh    requirements:      any_of:        - label: local-services          when:            context: host          tools:            psql: "*"          native:            - node-native-build-tools          env:            - DATABASE_URL          checks:            - workspace-dependencies-installed        - label: docker-services          when:            context: docker-host          tools:            docker: "*"          toolchains:            - node

Service-scoped lifecycle

Keep service-specific control and readiness with the service manager when Ota owns that service.

A task can still require the service without becoming the service owner.

Service scope exampleyaml
services:  postgres:    manager:      kind: compose    readiness:      from: app      run: pg_isready -h postgres -p 5432

Service lifecycle vs task prerequisites

Services own lifecycle and readiness. Tasks can ask for those services without turning them into dependency edges.

That keeps the service canonical and keeps the task graph honest.

Own the service

Service-owned lifecycle

Use this when Ota should own service lifecycle and readiness instead of leaving that work to a task wrapper.

When to use it

Choose this when the service is canonical infrastructure, not a task shell.

Depend on it

Task-declared prerequisite

Use this when a task should wait for a ready service before it runs, without turning the service into a dependency edge.

When to use it

Choose this when the task is the unit of work and the service should stay under services.

  • When the task is setup, setup.requires_services also becomes the pre-setup service phase for ota up: ota starts and verifies those services before setup, then brings the remaining required services up after setup.
Task prerequisite exampleyaml
services:  postgres:    manager:      kind: composetasks:  db:integration:    context: app    requires_services:      - postgres    run: mvn test

Native, container, and remote

Pick the execution plane that matches the real work.

Native

Use native when the repo should run directly on the host OS and its local toolchain.

When to use it

Choose this for host-first loops and simple local execution.

Container

Use container when the app or task needs an isolated image, deterministic toolchain, or shared compose-backed services.

When to use it

Choose this when the workload should run inside a container boundary.

Remote

Use remote when execution must happen on a declared transport target instead of the local machine.

When to use it

Choose this when the repo should run on a remote backend such as SSH, Daytona, or kubectl.

How Ota turns the choice into payoff

  • ota doctor shows the missing piece first, so the decision is visible instead of implicit.
  • ota up now uses setup-aware service orchestration: setup.requires_services runs first as the pre-setup service phase, then setup runs, then ota starts the remaining required services before final readiness diagnosis.
  • ota run can demand ready services by name, so tasks do not need to duplicate service ownership.
  • Receipts and JSON keep the same truth for humans, CI, and agents, so the contract stays reviewable.
Typical flowbash
ota doctorota upota run db:integrationota receipt --json

Common mistakes

  • Do not put Docker on the top level just because one context needs it.
  • Do not use depends_on when you mean a service prerequisite; use requires_services instead.
  • Do not turn services into ordinary tasks just to get a startup order.
  • Do not use compose:up as the canonical service path once typed managers exist.
Anti-patternyaml
tasks:  compose:up:    run: docker compose up -d  db:integration:    depends_on:      - compose:up