Skip to main content

Write Braces in Your Steps, and Trust Your Command Inputs

· 3 min read
Erik Osterman
Founder @ Cloud Posse

Every step in a custom command or workflow passes through Go templates before it runs. That is convenient until a script needs literal braces: a Starlark format string, a Helm-style placeholder, or {{ }} text meant for another tool. Escaping does not help, because a custom command renders its steps more than once, and atmos.yaml refused the !literal function that stack manifests already support. Custom-command inputs had rough edges too: an argument containing a comma was split in two, integer flags never reached the step, and a step's timeout: was not enforced.

The Problem​

Templates are rendered over the whole step definition, including the script body. A step with a line such as print("{{ name }}") failed before it ever ran, and the error named an internal template instead of your step. The same rendering also ran over the environment Atmos inherited from your shell, so a variable that happened to contain braces could break an unrelated script step or reach child processes rewritten.

Custom commands also had input problems that were easy to miss:

  • An argument value such as api,v2 arrived split into separate arguments.
  • A flag declared with type: int showed up as a string flag and was missing from the values passed to steps.
  • An optional argument without a default failed as if it were required.
  • timeout: on script, shell, and Atmos steps was accepted but never enforced, and an unknown output: mode was silently ignored.

The Fix​

Tag any step field with !literal, in atmos.yaml or in a workflow file, and Atmos uses it exactly as written. It works for script, command, interpreter, working_directory, and individual env values, in sequential steps, parallel and matrix children, workflows, and lifecycle hooks.

Atmos now renders only the environment values you declare; everything inherited from your shell passes through unchanged. Template errors name the step, the field, and the included file, and suggest !literal when the body contains braces. Parallel and matrix children render with the same template functions as sequential steps.

For custom commands, argument values arrive exactly as typed, integer flags are typed values, and optional arguments can omit a default. Step timeout: values are enforced, and output: accepts only raw, log, viewport, or none.

How to Use It​

Mark a script body as literal when it contains braces:

commands:
- name: greet
description: Print a greeting
arguments:
- name: name
description: Who to greet
required: false
flags:
- name: times
type: int
default: 1
description: How many greetings
steps:
- name: greet
type: script
interpreter: starlark
timeout: 30s
script: !literal |
name = ctx.arguments["name"] or "world"
for _ in range(ctx.flags["times"]):
print("{{ Hello }}, %s!" % name)

Running atmos greet platform --times=2 prints {{ Hello }}, platform! twice. The script reads its inputs through ctx.arguments and ctx.flags, so nothing is interpolated into the source. See the script step reference for details.

Get Involved​

Tell us how you use templates and scripts together in GitHub Discussions, or open an issue if a step field still renders when you expect it to stay literal.