Command
What it does
Command runs an arbitrary program or shell command from a monochange workflow.
This is the escape hatch step that lets you combine monochange’s structured state with the rest of your toolchain.
Why use it
Use Command when you need to:
- run project-specific tooling that monochange does not own
- upload artifacts
- call deployment, chat, or notification tools
- bridge monochange release context into custom scripts
- compose outputs from earlier steps into external automation
The important design rule is this:
prefer a built-in step whenever monochange already has a first-class semantic for the work.
Use Command for what is truly custom.
Core fields
command: the command to run in normal modewhen: optional boolean condition controlling whether the step runsdry_run_command: optional replacement command used only when the command runs with--dry-runshell: whether to run through a shell (true,false, or a custom shell binary name)id: optional identifier that exposessteps.<id>.stdoutandsteps.<id>.stderrto later stepsvariables: optional custom variable mapping for command substitutioninputs: optional step-local input overridesshow_progress: optional boolean; set tofalsewhen the command itself is interactive and spinner output would get in the wayinteractive: optional step input; when set totruethe step runs with inherited stdio so the command can own the terminal, and the progress spinner is suppressed for the step. This is the recommended way to run interactive tools such as wizards or TUIs; unlikeshow_progress = false, the command can read from stdin and render directly to the terminal. Output is not captured, sosteps.<id>.stdoutandsteps.<id>.stderrare empty for interactive steps
Example: publish through a configured workflow while keeping an interactive terminal available for prompts. Declare the boolean input, pass it to the step, and set it with --interactive when you run locally; CI stays non-interactive by leaving it unset.
[cli.publish]
help_text = "Publish packages"
[[cli.publish.inputs]]
name = "interactive"
type = "boolean"
default = false
[[cli.publish.steps]]
name = "publish"
type = "Command"
command = "npm publish"
inputs = ["interactive"]
# local run with the terminal handed to the command
monochange run publish --interactive
# CI run without a terminal
monochange run publish
always_run: optional boolean; set totrueto run this step even when a previous step has failed
Prerequisites
Command itself has no built-in prerequisite.
What it can see depends on where you place it:
- after
PrepareRelease, it can consumerelease.*andmanifest.path - after
AffectedPackages, it can consumeaffected.* - after
RetargetRelease, it can consumeretarget.* - after
CommitRelease, it can consumerelease_commit.* - after another named
Command, it can consumesteps.<id>.*
Side effects and outputs
- runs an external command
- records stdout/stderr when
idis present - can act as a consumer or producer step in a workflow chain
Example
[cli.test]
help_text = "Run project tests"
[[cli.test.steps]]
type = "Command"
command = "cargo test --workspace --all-features"
dry_run_command = "cargo test --workspace --all-features --no-run"
shell = true
Composition ideas
Consume prepared release context
[cli.release-with-notes]
help_text = "Prepare a release and print a custom summary"
[[cli.release-with-notes.inputs]]
name = "format"
type = "choice"
choices = ["text", "json", "json-min"]
default = "text"
[[cli.release-with-notes.steps]]
type = "PrepareRelease"
inputs = ["format"]
[[cli.release-with-notes.steps]]
type = "Command"
command = "echo Releasing {{ release.version }} for {{ released_packages }}"
shell = true
Reuse earlier command output
[cli.release-with-generated-notes]
help_text = "Prepare a release, generate notes, and upload them"
[[cli.release-with-generated-notes.steps]]
type = "PrepareRelease"
[[cli.release-with-generated-notes.steps]]
type = "Command"
id = "notes"
command = "printf 'version=%s\n' '{{ release.version }}'"
shell = true
[[cli.release-with-generated-notes.steps]]
type = "Command"
command = "printf '%s\n' '{{ steps.notes.stdout }}'"
shell = true
Consume repair state
[cli.repair-and-notify]
help_text = "Repair a release and print the retarget result"
[[cli.repair-and-notify.inputs]]
name = "from"
type = "string"
required = true
[[cli.repair-and-notify.inputs]]
name = "target"
type = "string"
default = "HEAD"
[[cli.repair-and-notify.steps]]
type = "RetargetRelease"
inputs = ["from", "target"]
[[cli.repair-and-notify.steps]]
type = "Command"
command = "echo moved {{ retarget.tags }} to {{ retarget.target }} with status {{ retarget.status }}"
shell = true
Why choose Command carefully?
Because it is powerful enough to bypass monochange’s typed guarantees.
That is useful, but it also means:
- validation cannot reason deeply about your command string
- provider-aware dry-run semantics are now partly your responsibility
- shell quoting and portability become part of the workflow design
Recommended usage pattern
A good workflow usually looks like this:
- use built-in steps to create stable state
- use
Commandonly for the final custom integration points - give important custom steps an
idso later steps can consume structured stdout
Common mistakes
- using
Commandto reimplementPublishReleaseorOpenReleaseRequest - forgetting
dry_run_commandwhen the real command would mutate external systems - omitting
idand then having no clean way to reuse the command’s output later - relying on shell features without setting
shell = trueor a custom shell name