Skip to main content

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:

examples/starlark-script/summarize.star

Save the input beside it as services.json:

examples/starlark-script/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.

Custom CLI app: ./summarize.star services.json
 
00:00.0 / 00:00.0

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:

examples/starlark-script/capacity.star

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_list flag splits on commas and honors CSV quoting, so DEPLOY_ZONES=a,b is ["a", "b"] and DEPLOY_ZONES='"a,b",c' is ["a,b", "c"]. Whitespace is not a separator. With choices, every element is checked.
  • An int flag accepts base-10 digits only, from the command line and from the environment. 010 is ten, and 0x10 is rejected instead of being read as sixteen.
  • Names are case-insensitive. A script that declares both Stage and stage, or two arguments that differ only by case, fails when cli.command runs.

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 starlark filetype. Its built-in Starlark support is the bzl filetype, so use # vim: set filetype=bzl:. Vim checks the first and last five lines by default (modelines=5) when modeline is 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 starlark language, then associate your files in settings.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 starlark and bazel, so # vim: set ft=starlark: or # -*- mode: starlark -*- is detected, while bzl and bazel-starlark are not. Because those two values differ from what Vim and Emacs need, the most reliable option for an extensionless file is a .gitattributes entry such as bin/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.container to build and publish an image, then atmos.terraform or atmos.helm to 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 to atmos.* run through the Atmos executable and use the same configuration and authentication as their CLI equivalents. Use atmos.auth to 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.parallel to check several stacks or components concurrently. Set a concurrency limit and add retries and timeouts with steps.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, and regex.search to process files and command results. exec.run launches external programs and returns their output and exit code for your script to inspect.

Give operators useful output

Write progress with ui.info and ui.success, diagnostics with log.debug, and data with print. 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.build for 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 output to 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.