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:
#!/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:
#!/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_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 <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
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.
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:
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.
Example: validate arguments and flags
This capacity tool accepts a service name and a replica count:
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.