# 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](/automation/testing), 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](/automation/language#why-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](/automation/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:

```shell
chmod +x summarize.star
./summarize.star services.json
```

```text
▶ 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:

```shell
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 `-`:

```shell
atmos --interpreter=starlark - services.json < summarize.star
```

Pipes and here-documents work too:

```shell
atmos - foo bar <<'EOF'
print(ctx.args)
EOF
```

```text
["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:

```shell
chmod +x capacity.star
./capacity.star api --replicas=3
```

```text
✓ 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]`:

```text
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:

```text
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:

```shell
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:

```shell
./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:

```python
#!/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](https://github.com/bazelbuild/emacs-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](https://github.com/github-linguist/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`](/functions/automation/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`](/functions/automation/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`](/functions/automation/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:

```python
#!/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](/functions/automation) for signatures
and examples, and [secret masking](/cli/configuration/settings/mask) 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`](/cli/commands/ai/skill).

## Call the step library

A standalone program can use the same [step handlers](/functions/automation/steps.run)
as a YAML workflow. Save this as `select-service.star`:

```python
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](/steps/type/script#declaring-a-standalone-command)
for supported flag types and parsing rules, and the
[language overview](/automation/language) for helpers,
module loading, processes, and parallel tasks.

Browse the [complete runnable example](/examples/starlark-script) for both scripts
and their input data. To expose your automation under the Atmos CLI itself,
continue with [custom commands](/automation/custom-commands).
