Reference

Task Bodies

When to use prepare, command, launch, run, script, action, and aggregate.

referenceautomation buildersintermediatestable2026-05-30

Decision order

Do not start from run by habit.

Start from the strongest surface that truthfully matches the task boundary, then fall back to shell only when the structured surfaces would lie.

In the normal case, a task declares exactly one body: prepare, command, launch, run, script, action, or aggregate.

  • use prepare when Ota owns the setup lane itself
  • use command when the task is one finite executable plus a stable argument vector
  • use launch.kind: command when the task starts a long-running service process
  • use action when the task is a deterministic built-in mutation such as env-file or network preparation
  • use aggregate when the task body is only grouping other tasks
  • use run only for finite shell shorthand or simple compatibility cases
  • use script only for multiline or truly shell-shaped escape hatches

Contract rule

This page is a choice guide, but the validator also enforces a concrete contract rule.

Treat task bodies as mutually exclusive execution ownership, not fields to mix together.

  • base tasks normally declare exactly one body: run, script, command, prepare, launch, action, or aggregate
  • a base task may omit the body only when execution-mode inheritance or OS-variant selection intentionally supplies it
  • OS variants are narrower: each variant declares exactly one of run, script, or command
  • execution-mode branches are also narrower than full task bodies: they override run, script, command, prepare, or launch
  • aggregate is mutually exclusive with the other task bodies and should not be mixed with executable task-local execution fields

Prepare

prepare is the first-class setup body that Ota owns directly.

Use it when the task truth is dependency hydration, tool bootstrap, or another typed setup lane that should stay governed as contract data instead of being hidden inside shell.

  • choose prepare when Ota already ships the setup primitive truthfully
  • use it for install and bootstrap lanes that mutate the repo in a known, structured way
  • prefer it over run or script when shell would only be carrying package-manager or toolchain glue
Prepare exampleyaml
tasks:  setup:    prepare:      kind: dependency_hydration      medium: package_dependencies      source:        kind: cargo        cwd: .    requirements:      toolchains:        - rust

Command

command is the structured finite execution surface for one executable plus a stable argument vector.

Use it when the task really is one argv-shaped command such as cargo clippy, uv run pytest, npm run build, or docker compose down.

  • choose command when shell parsing is not the real boundary
  • use it when Ota should be able to inspect and govern the executable and args structurally
  • prefer it over run when the task is one finite executable and not a shell pipeline or shell-control-flow lane
Command exampleyaml
tasks:  lint:    command:      exe: cargo      args:        - clippy        - --all-targets        - --no-deps

Launch.kind: command

launch.kind: command is the structured long-running process surface.

Use it when the task starts a service process and Ota should own startup, interruption, runtime identity, listeners or surfaces, and readiness cleanly.

  • choose it for service starts, not for finite verification or build commands
  • use it when the process is meant to stay alive until interrupted
  • prefer it over run or script for services so runtime ownership stays explicit instead of opaque shell glue
Launch exampleyaml
tasks:  dev:    launch:      kind: command      exe: bundle      args:        - exec        - rails        - server        - -b        - 0.0.0.0        - -p        - "3000"    runtime:      kind: service      surfaces:        api:          project:            host:              primary: true

Run

run is a one-line shell body.

Use it only when the task is still finite but shell behavior is the honest shape, for example a pipeline, shell builtin, variable expansion, or a compatibility lane that is not cleanly representable as structured argv.

  • choose run for finite shell shorthand, not by default
  • keep it when the shell itself is semantically doing real work
  • do not move a task to command if quoting, piping, or shell expansion is required for correctness
Run exampleyaml
tasks:  test:    description: Validate the prepared state    depends_on:      - setup    run: test -f .ota/state.txt

Script

script is the multiline shell escape hatch.

Use it only when the task really needs multiline shell flow and splitting it into fake structured fields would make the contract less truthful.

  • choose script for multiline finite shell work, not for long-running services
  • keep it when conditionals, setup steps, cleanup, or shell control flow are the real task boundary
  • do not use it just because a task has more than one line if Ota already has a stronger structured surface
Script exampleyaml
tasks:  setup:    description: Prepare generated files with an inline shell script    script: |      mkdir -p .ota      printf ready > .ota/state.txt

Action

action is the deterministic built-in mutation surface that Ota executes without shell glue.

Use it when the task owns a small governed mutation such as env-file bootstrap, container network preparation, or a deterministic bundle of built-in actions.

  • choose action when Ota already owns the mutation primitive
  • use it for tiny deterministic mutations that should stay machine-readable and policy-friendly
  • prefer it over shell for copy-or-create bootstrap lanes when the built-in shape is already supported
Action exampleyaml
tasks:  setup:env:local:    internal: true    action:      kind: ensure_env_file      path: .env.local      template: .env.example      vars:        APP_ENV:          value: local          mode: replace        APP_SECRET:          random:            bytes: 24            encoding: hex

Aggregate

aggregate is the grouping body that runs child tasks and owns no command itself.

Use it when the task is just a named verification or orchestration bundle and would otherwise fake a body with run: "true" or another placeholder shell command.

  • choose aggregate when the value is the named grouping, not a command body
  • use it for verification bundles, release check bundles, or other orchestration identities
  • prefer it over wrapper shell tasks that only dispatch to child tasks
Aggregate exampleyaml
tasks:  verify:    aggregate:      tasks:        - lint        - build        - test

Prefer this over that

  • prefer command over run when the body is one executable and argv, because Ota can inspect and govern it structurally
  • prefer launch.kind: command over run or script for services, because Ota can then own startup, interruption, and readiness cleanly
  • prefer prepare over install shell glue when Ota already ships the setup lane truthfully
  • prefer action over tiny imperative shell mutations when Ota already owns the built-in mutation
  • prefer aggregate over fake wrapper tasks such as run: "true"

Shell is still valid when it is honest

The goal is not to ban shell. The goal is to stop hiding structured truth inside shell when Ota already has a stronger surface.

  • keep run for finite shell pipelines such as foo | bar
  • keep run or script when shell variable expansion, conditionals, or builtins are the real task boundary
  • keep script for multiline finite escape hatches that would become less truthful if split into fake structured fields
  • do not move a task to command if quoting, redirection, or shell control flow is semantically required

Governance rule

Current Ota governance already pushes service tasks away from opaque shell starts, and it now also warns on obvious shell lanes that are replaceable by stronger structured ownership.

  • if a task is a long-running service, expect ota validate and ota doctor to push it toward launch.kind: command
  • if a finite shell task is obviously one executable plus argv, expect governance to push it toward command
  • if a shell setup lane is obviously a shipped dependency-hydration family, expect governance to push it toward prepare.kind: dependency_hydration
  • treat run and script as honest escape hatches, not the default authoring posture