Skip to main content

Custom CLI Apps

A custom CLI app is an executable script with its own arguments, flags, and help. Add an Atmos shebang, make the file executable, and run it as ./myapp.

Use standalone apps for project tools, reports, or release commands that run locally and in CI. Atmos supplies the Starlark interpreter and built-in helpers. Scripts using only those helpers need no atmos.yaml, stacks, or separate language runtime. External commands still need their tools and credentials.

To add a subcommand such as atmos capacity, use custom commands.

Executable syntax​

Put Atmos on your PATH and save this as myapp:

myapp
#!/usr/bin/env atmos
ui.success("Hello from myapp")

Make it executable and run it:

chmod +x myapp
./myapp

The first line selects Atmos as the interpreter. No file extension is required. A .star extension is optional and can help editors recognize Starlark. On systems without shebang support, use atmos ./myapp.

Declare arguments, flags, and help​

Use cli.command to declare the inputs and the function that runs the app. cli.arg defines a positional argument; cli.flag defines a named option:

myapp
#!/usr/bin/env atmos

def main(args, flags):
ui.success("{}, {}!".format(flags["greeting"], args["name"]))

cli.command(
description = "Print a greeting",
args = [cli.arg("name")],
flags = [cli.flag("greeting", default="Hello")],
run = main,
)

Run ./myapp world --greeting Hi. Use ./myapp --help to list the argument and flag. Scripts without cli.command receive raw arguments in ctx.args.

Built-in helpers​

Use components.get to read resolved stack configuration, atmos.* to run Atmos commands, and exec.run to call external tools. dependencies.tools installs pinned tool versions for processes the script starts; installation may need network access. atmos.auth runs commands with a configured identity.

Process files with fs, json, and regex. Run independent functions with steps.parallel; add retries and timeouts with steps.task. Results retain input order. Call registered step handlers through steps, including prompts and HTTP requests.

Use print for data, ui for status, and log for diagnostics. Assign output to return a value, or use errors.build to report an error with recovery hints. See the function reference and testing guide.

Work with parsed inputs​

The run and optional validate callbacks receive args and flags dictionaries. Arguments and flags retain their declared types. Use their values directly in the script.

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 <service> [flags]

Arguments:
<service> Service to deploy

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, arguments, and flags.

Unless you pass name to cli.command, the command name shown in usage is the script's file name without the .star extension, so release.star appears as release.

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 <service> [flags]
💡 Run ./release.star --help for usage.

A validate callback that returns False is also a usage error, with status 2. Errors your own code raises, such as fail() (including fail() inside validate, which exits with status 1 and shows its reason) or a runtime error, 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.

Interrupt with Ctrl-C​

The first Ctrl-C cancels the script, runs its defer calls with a 30 second grace period, and exits with status 130 without printing an error box. A second Ctrl-C exits immediately.

Global flags​

Atmos global flags go between atmos and the script path. Flags that take a value accept the --flag=value form, and most also accept 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 or --force-color. A relative script path is resolved against the directory you were in before --chdir (or ATMOS_CHDIR) moved the process, because that is the directory you typed it against. The script then runs in the new working directory. Usage hints keep the spelling you typed. The --profile and --identity flags must use the --flag=value form. A space form such as --profile dev before the script is rejected with a hint to write --profile=dev, because the word dev would otherwise be taken as the script path. 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. The extension match ignores case, so TOOL.STAR and Tool.Star run like tool.star. 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 shebang can carry Atmos global flags. Both first lines below run the file as an Atmos script:

#!/usr/bin/env atmos
#!/usr/bin/env -S atmos --logs-level=Debug

The -S option of env splits the line into separate words, so the flags that follow atmos reach Atmos before the script path, where they apply like any global flag written between atmos and the script. env also accepts NAME=value assignments ahead of atmos.

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.

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"]

When standard input is a terminal, Atmos prints Reading script from stdin; press Ctrl-D to end. on stderr so you know it is waiting for the script. Pipes and here-documents print nothing.

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.

Example: summarize a service manifest​

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

Example: validate arguments and flags​

This capacity tool accepts a service name and a replica count:

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.

Example: release an application​

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, run chmod +x release, then run ./release --stack dev. Use ./release --help to list its flags 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.

Example: prompt for input​

A standalone program can use the same step handlers as a YAML workflow. Save this as select-service:

#!/usr/bin/env atmos
service = steps.input(prompt = "Service name?", default = "api").value
ui.success("Selected " + service)

Run chmod +x select-service, then ./select-service 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.