Skip to content

Step execution types

Every leaf pipeline step declares a type: that selects how its cmd: is executed.

Executes a shell command via sh -c. Full shell semantics apply: environment variable expansion, globbing, pipes, redirection, and &&/|| operators all work as expected.

- name: chmod-scripts
type: shell
cmd: chmod +x scripts/deploy.sh

cmd: shell (builtin) vs type: shell (step)

Section titled “cmd: shell (builtin) vs type: shell (step)”

The shell builtin (cmd: shell) is distinct from the step execution type (type: shell). Both execute shell commands, but with different portability guarantees:

Step type: type: shell — Uses the project’s configured shell (via config.ShellBin) for maximum flexibility. If the project has set a custom shell binary (e.g., zsh instead of sh), step bodies use that shell.

- name: run-with-project-shell
type: shell
cmd: some-zsh-specific-feature-here

Builtin: cmd: shell — Uses POSIX-portable hardcoded sh -c for maximum predictability. Used in two contexts:

  1. As a step body (less common):
- name: check-docker-login
type: builtin
cmd: shell
with:
cmd: docker info | grep -q ghcr.io
timeout: 10s
  1. As a pre/post-condition (common in deploy and validate):
- name: copy-configs
type: builtin
cmd: service_configs_copy
# ...
when:
type: shell
cmd: "test -f templates/config.default"
check:
type: builtin
cmd: shell
with:
cmd: "test -f services/main/configs/app.conf"

Both usages ensure that conditions evaluate portably across CI systems, container runtimes, and developer shells, regardless of the project’s config.ShellBin setting. See validate.yml for the full cmd: shell builtin documentation.

Working directory. Both the type: shell step body and the cmd: shell builtin run with their working directory set to the project root — the same base when: conditions and the file_exists builtin use. A relative path in a step body therefore names the same file as the relative path in the check: guarding it, regardless of the subdirectory dwe was invoked from.

Timeout. The cmd: shell builtin takes an optional timeout: (default 10s). timeout: "0" means unbounded, matching a step’s own timeout: convention — this is what lets a derived check: auto keep the unbounded posture of the when: it inverts. A negative duration ("-5s") is rejected, exactly as the step-level timeout: is — 0 is the only unbounded spelling. type: shell step bodies have no builtin timeout; bound them with the step-level timeout: field.

Invokes a DWE CLI subcommand. The binary path is resolved automatically.

- name: up
type: dwe
cmd: "docker up"
- name: info
type: dwe
cmd: "info"
- name: render-ide
type: dwe
cmd: "render ide main"

Dispatches a declarative command by ID from the command registry (workspace/commands/).

- name: composer-install
type: command
cmd: services.main.composer-install
- name: db-create
type: command
cmd: services.main.db.create
with:
database: laravel_test

Executes an engine-internal Go function. Builtins run in-process and have access to the full config. The same registry is reachable from declarative commands via type: builtin in commands/ — pipelines and commands share one set of builtins.

- name: create-dirs
type: builtin
cmd: service_dirs_ensure
with:
service: main
mode: skip
- name: success-msg
type: builtin
cmd: message
with:
level: success
text: "Deploy completed"

See Available builtins for the full registry and parameter reference.

Predicate builtins as step bodies (assertion semantics)

Section titled “Predicate builtins as step bodies (assertion semantics)”

Most builtins are actions (they do something). Some are predicates — they answer a yes/no question about the world (file_exists, executable_in_path, tcp_reachable, http_check, containers_running, env_keys_present, config_keys_present, and the shell builtin). A predicate may be used as a step body, where it behaves as an assertion:

  • The check passes → the step succeeds.
  • The check fails → the step fails with the predicate’s own message, halting the pipeline.
- name: assert-seed-present
type: builtin
cmd: file_exists
with:
path: .dwe/seed.sql

Assertion steps are always re-run — deploy’s “already up-to-date” gate and the per-step action-hash skip never skip a predicate-body step (the same treatment check: steps receive), because an assertion has no meaningful cached result. A when: guard still applies: a predicate-body step whose when: is false is skipped without asserting.

See the predicate-as-body preamble in the builtins reference for the full list and rationale.