Custom CLI Apps
Give your team one command to build, ship, and deploy containers, locally or in
CI. Write your CLI app in the Atmos Automation Language, a Python-like DSL based
on Starlark. Save it as a .star file and run it with atmos ./my-tool.star.
A release tool can build an image, push it to your registry, and deploy it using your project's Atmos stacks, identities, and toolchain. Commit the tool with your project and invoke it from both your terminal and CI. Changes to the release process live in one place, and you can exercise them locally before a pipeline runs.
As Bash wrappers grow, you end up maintaining argument parsing, quoting, JSON processing, and coordination between background processes. Atmos supplies typed inputs, structured values, parallel tasks, and generated help. Put shared logic in functions and test it, including success and failure cases. The same test steps can check command results and deployed services.
Starlark provides functions, loops, and structured data with familiar Python-like syntax. The interpreter ships with Atmos, so scripts using its built-ins need no separate language runtime. See why Atmos uses Starlark for the language's design and capabilities.
Install Atmos and put it on your PATH before starting. The first two examples
use only built-in functions; they need no atmos.yaml, stacks, Python, or other
tools. To register a subcommand such as atmos capacity in a project's
configuration, see custom commands.
Start with an executable script
Save this program as summarize.star. It reads the input filename from
ctx.args, loads the JSON file, and reports the total number of replicas:
Save the input beside it as services.json:
Run from the directory containing both files. The shebang on the first line selects Atmos as the interpreter:
chmod +x summarize.star
./summarize.star services.json
▶ api: 2 replicas
▶ worker: 3 replicas
✓ Total: 2 services, 5 replicas
The ui.info calls report each service, and ui.success highlights the total. These styled
messages use stderr, leaving stdout available for data.
On systems without shebang support, run atmos ./summarize.star services.json.
Input paths passed to fs.read_file are relative to your working directory.
Read a script from stdin
Use - in place of a script filename:
atmos - services.json < summarize.star
atmos - -- services.json < summarize.star
Both commands pass services.json to the script. The optional -- immediately
following - is consumed by Atmos; subsequent arguments and flags belong to the
script. For a script that declares cli.command, atmos - --help < capacity.star
shows that script's help.
Starlark is the default interpreter for stdin. To select it explicitly, put
--interpreter before -:
atmos --interpreter=starlark - services.json < summarize.star
Pipes and here-documents work too:
atmos - foo bar <<'EOF'
print(ctx.args)
EOF
["foo", "bar"]
Atmos reads the source through EOF before executing it. Relative load() imports
and file operations resolve from the working directory. There is no source
filename, so ctx.script is None, and tracebacks identify the source as
<stdin>. An unnamed cli.command uses stdin as its command name.
The - marker is required: atmos < summarize.star does not select script
execution. --interpreter selects a registered embedded interpreter; it does not
launch an external interpreter.
Declare arguments, flags, and help
When a tool needs a documented interface, declare it with cli.command.
Use cli.arg for a positional argument and cli.flag for a named option.
Atmos parses those inputs before calling your program:
Save this example as capacity.star, make it executable, and run:
chmod +x capacity.star
./capacity.star api --replicas=3
✓ api: 3 replicas x 4 workers = 12 workers
Omitting --replicas uses the declared default of two. A value such as
--replicas=many fails integer parsing, while --replicas=0 parses as an integer
but fails the program's positive-count validation.
Run ./capacity.star --help to see the required service argument, replica flag,
and default. Help runs neither the validation callback nor main.
Work with parsed inputs
The run and optional validate callbacks receive args and flags
dictionaries. In this example, args["service"] is a string and
flags["replicas"] is already an integer. Use these values directly in your
program.
Flags can declare defaults, choices, and environment bindings. Use a validation
callback for rules beyond a flag's type, such as a positive count or a relationship
between two inputs. Keep the work itself in the run callback so help and invalid
invocations do not start it.
Environment values parse exactly like command-line values:
- A
string_listflag splits on commas and honors CSV quoting, soDEPLOY_ZONES=a,bis["a", "b"]andDEPLOY_ZONES='"a,b",c'is["a,b", "c"]. Whitespace is not a separator. Withchoices, every element is checked. - An
intflag accepts base-10 digits only, from the command line and from the environment.010is ten, and0x10is rejected instead of being read as sixteen. - Names are case-insensitive. A script that declares both
Stageandstage, or two arguments that differ only by case, fails whencli.commandruns.
A bool flag never consumes the next word. --verbose false would turn the flag on
and pass false as a positional argument, so Atmos rejects it. Write
--verbose=false, or put -- before an argument you mean literally.
Help and usage errors
--help lists every flag with what you need to call it correctly: required flags
are marked (required), flags with choices show (one of: dev, prod), and a
flag bound to an environment variable shows [env: DEPLOY_STAGE]:
Usage:
release.star <service> [flags]
Flags:
-h, --help Show help for this script
--stage string Stage to deploy (one of: dev, prod) [env: DEPLOY_STAGE] (default "dev")
--token string API token (required) [env: DEPLOY_TOKEN]
Help is compact and shows only your tool's own usage and flags.
Mistakes in the command line are usage errors, not script crashes. An unknown flag,
a missing or surplus argument, an invalid value, a missing required flag, or a value
outside choices prints the problem, the usage line, and a hint, with no Starlark
traceback, and exits with status 2:
Error: usage error
missing required argument <service>
Usage: release.star <service> [flags]
💡 Run ./release.star --help for usage.
Errors your own code raises, such as fail(), a runtime error, or a validate
callback returning False, keep the Starlark presentation with a traceback.
Tracebacks show paths relative to the working directory when the script is under
it, and log fields use the script's file name, such as step=release.star.
Output
Your program prints data by assigning the top-level output. A string is written as
is, and any other value is written as JSON. None means no output, so
output = cli.command(...) prints nothing when the callback returns nothing or when
you ask for --help.
Global flags
Atmos global flags go between atmos and the script path, in either the
--flag=value or the --flag value form:
atmos --chdir=tools ./release.star api
atmos --logs-level=Debug ./release.star api
Atmos applies these flags itself, and the first word that is not a flag starts the
script. Everything after the script path belongs to the script, including flags
that look like Atmos flags, such as a script-owned --chdir. A relative script
path is resolved after --chdir is applied, so it is relative to the new working
directory. Flags that take an optional value, such as --identity, use the
--flag=value form. A flag Atmos does not define before the script is an error
from the normal command line.
With ATMOS_USE_VERSION or version.use, Atmos re-runs your script with the pinned
version and forwards the script path and its arguments unchanged.
Paths with a registered extension, such as .star, report script errors. An existing directory
such as ./stacks is left to normal Atmos command handling.
File names and editor support
A script runs by path, so the .star extension is optional. These all run the script:
./tool # Shebang: #!/usr/bin/env atmos
atmos ./tool # A path, with or without .star
atmos tool.star
A bare atmos tool with no slash and no .star is an Atmos command name, so it
never runs a script. Use ./tool, atmos ./tool, or a .star name.
The .star extension is what lets editors and GitHub recognize a file as Starlark
without any other hint. An extensionless script needs a file-type hint. Put the
shebang on line 1 and the hint on line 2, because Vim, Emacs, and GitHub all search
the first lines of a file:
#!/usr/bin/env atmos
# vim: set filetype=bzl: -*- mode: bazel-starlark -*-
def main(args, flags):
ui.success("Hello, " + args["name"] + "!")
cli.command(
description = "Greet someone",
args = [cli.arg("name")],
run = main,
)
Atmos reads the shebang on line 1 and treats line 2 as a comment, so the hint does not change how the script runs.
- Vim
Vim has no
starlarkfiletype. Its built-in Starlark support is thebzlfiletype, so use# vim: set filetype=bzl:. Vim checks the first and last five lines by default (modelines=5) whenmodelineis on.- Emacs
Emacs has no built-in Starlark mode. With the bazel-mode package installed, use
-*- mode: bazel-starlark -*-. Without it,-*- mode: python -*-gives close syntax highlighting. When line 1 is a#!line, Emacs reads the mode line from line 2.- VS Code
VS Code has no built-in Starlark language. Install an extension that provides the
starlarklanguage, then associate your files insettings.json, for example"files.associations": { "**/bin/tool": "starlark" }.- GitHub
GitHub's Linguist reads Vim and Emacs modelines in the first and last five lines, and resolves the value as a language name or alias. The Starlark language answers to
starlarkandbazel, so# vim: set ft=starlark:or# -*- mode: starlark -*-is detected, whilebzlandbazel-starlarkare not. Because those two values differ from what Vim and Emacs need, the most reliable option for an extensionless file is a.gitattributesentry such asbin/tool linguist-language=Starlark.
What your program can do
Use Atmos's built-in functions to turn a small script into a tool that coordinates your infrastructure. Each capability uses the configuration and execution controls already available through Atmos:
- Build a deployment tool
Call
atmos.containerto build and publish an image, thenatmos.terraformoratmos.helmto deploy a component. Give the sequence one command-line interface with the inputs and defaults your team needs.- Use your existing stacks and identities
Read merged variables, settings, and metadata with
components.get. Calls toatmos.*run through the Atmos executable and use the same configuration and authentication as their CLI equivalents. Useatmos.authto authenticate with a configured identity and run commands in its credential environment.- Declare the tools your app needs
Pin tool versions with
dependencies.tools. Atmos installs missing versions and adds them to the script's process environment, making the requested versions available to commands your app starts. Installation may need network access.- Run independent work in parallel
Use
steps.parallelto check several stacks or components concurrently. Set a concurrency limit and add retries and timeouts withsteps.task. Results retain input order, and streamed output identifies the task that produced it.- Read data and call other tools
Use
fs.read_file,json.decode, andregex.searchto process files and command results.exec.runlaunches external programs and returns their output and exit code for your script to inspect.- Give operators useful output
Write progress with
ui.infoandui.success, diagnostics withlog.debug, and data withprint. Status goes to stderr, leaving stdout for another tool to consume. When masking is enabled, Atmos masks registered secrets and matches for configured masking patterns in displayed output.- Make failures actionable
Use
errors.buildfor errors with explanations, recovery hints, examples, and context. They use Atmos's error formatting and reporting, including Sentry when configured.- Return results other tools can use
Assign
outputto return a string or structured value. Standalone apps write that result to stdout; scripts in workflows, custom commands, and hooks expose it as a step value.
For example, a release tool can build and publish an image, deploy it with
Terraform, and report the result. This program assumes your project defines
container and Terraform components named api in the selected stack, with the
required tools and credentials available:
#!/usr/bin/env atmos
def main(args, flags):
stack = flags["stack"]
ui.info("Releasing api to " + stack)
atmos.container("build", "api", flags={"stack": stack})
atmos.container("push", "api", flags={"stack": stack})
atmos.terraform("deploy", "api", stack)
ui.success("Released api to " + stack)
cli.command(
description = "Build, publish, and deploy the API",
flags = [cli.flag("stack", shorthand="s", required=True,
description="Stack to release to")],
run = main,
)
Save it as release.star and run atmos ./release.star --stack dev to execute
that release sequence. Run atmos ./release.star --help to display its interface
without starting the release.
See the automation function reference for signatures
and examples, and secret masking for masking
configuration. The bundled atmos-starlark agent skill documents the language
and APIs for coding agents; install it with
atmos ai skill install atmos-starlark.
Call the step library
A standalone program can use the same step handlers
as a YAML workflow. Save this as select-service.star:
service = steps.input(prompt = "Service name?", default = "api").value
ui.success("Selected " + service)
Run atmos ./select-service.star to prompt in a terminal. Without a terminal,
the input step returns its configured default. Read the result's .value,
.metadata, or .outputs to pass data to subsequent operations. The step library
also includes container operations, HTTP requests, archives, and formatted output.
Continue building
Read the command declaration reference for supported flag types and parsing rules, and the language overview for helpers, module loading, processes, and parallel tasks.
Browse the complete runnable example for both scripts and their input data. To expose your automation under the Atmos CLI itself, continue with custom commands.