Reference
Task Bodies
When to use prepare, command, launch, run, script, action, and aggregate.
Recommended next
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
preparewhen Ota owns the setup lane itself - use
commandwhen the task is one finite executable plus a stable argument vector - use
launch.kind: commandwhen the task starts a long-running service process - use
actionwhen the task is a deterministic built-in mutation such as env-file or network preparation - use
aggregatewhen the task body is only grouping other tasks - use
runonly for finite shell shorthand or simple compatibility cases - use
scriptonly 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, oraggregate - 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, orcommand - execution-mode branches are also narrower than full task bodies: they override
run,script,command,prepare, orlaunch aggregateis 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
preparewhen 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
runorscriptwhen shell would only be carrying package-manager or toolchain glue
tasks: setup: prepare: kind: dependency_hydration medium: package_dependencies source: kind: cargo cwd: . requirements: toolchains: - rustCommand
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
commandwhen 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
runwhen the task is one finite executable and not a shell pipeline or shell-control-flow lane
tasks: lint: command: exe: cargo args: - clippy - --all-targets - --no-depsLaunch.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
runorscriptfor services so runtime ownership stays explicit instead of opaque shell glue
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: trueRun
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
runfor finite shell shorthand, not by default - keep it when the shell itself is semantically doing real work
- do not move a task to
commandif quoting, piping, or shell expansion is required for correctness
tasks: test: description: Validate the prepared state depends_on: - setup run: test -f .ota/state.txtScript
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
scriptfor 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
tasks: setup: description: Prepare generated files with an inline shell script script: | mkdir -p .ota printf ready > .ota/state.txtAction
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
actionwhen 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
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: hexAggregate
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
aggregatewhen 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
tasks: verify: aggregate: tasks: - lint - build - testPrefer this over that
- prefer
commandoverrunwhen the body is one executable and argv, because Ota can inspect and govern it structurally - prefer
launch.kind: commandoverrunorscriptfor services, because Ota can then own startup, interruption, and readiness cleanly - prefer
prepareover install shell glue when Ota already ships the setup lane truthfully - prefer
actionover tiny imperative shell mutations when Ota already owns the built-in mutation - prefer
aggregateover fake wrapper tasks such asrun: "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
runfor finite shell pipelines such asfoo | bar - keep
runorscriptwhen shell variable expansion, conditionals, or builtins are the real task boundary - keep
scriptfor multiline finite escape hatches that would become less truthful if split into fake structured fields - do not move a task to
commandif 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 validateandota doctorto push it towardlaunch.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
runandscriptas honest escape hatches, not the default authoring posture