script
The script step type runs inline code with an explicit interpreter. Use it for a small,
portable script body instead of encoding a heredoc in a shell command.
steps:
- name: check generated files
type: script
interpreter: python3
working_directory: .
script: |
from pathlib import Path
if not Path("generated/config.yaml").exists():
raise SystemExit("missing generated config")
Fields
| Field | Description |
|---|---|
interpreter | Required executable, or starlark for the embedded runtime. Supports templates. |
script | Required inline program text. Supports templates. |
working_directory | Optional process directory. Supports templates. |
env | Optional environment map. Only these declared values are rendered as templates. |
output | Optional output mode: raw, log, viewport, or none. Any other value is rejected before the step runs. Script steps default to raw. |
show | Optional display settings such as show.labels. See Step labels. |
retry | Optional retry policy that reruns the whole step on failure. |
timeout | Optional duration such as 30s or 5m. Atmos stops the interpreter and its subprocesses when it elapses and fails the step with step timed out. The timeout bounds the whole step, including every retry attempt and the wait between attempts, so retries never extend it. |
when | Optional condition that decides whether the step runs. |
outputs | Optional named values derived from the step result, available as {{ .steps.<name>.outputs.<key> }}. |
container | Rejected for interpreter: starlark, which runs inside Atmos. See Containers and dry runs. |
Do not set command on a script step. Use type: shell for a real shell command and
type: script when the interpreter and body are the intentional interface.
Environment and templates
Atmos renders the values you declare under the step's env as templates. The ambient process
environment, which includes everything inherited from your shell, is never rendered. It
reaches the interpreter and its child processes exactly as it is, so a variable such as
FOO='{{ bad' neither fails the step nor changes value. Use
!literal on a declared env value to keep that value unrendered too.
Keep braces out of template rendering
Atmos renders the script body as a Go template before the interpreter runs. Expressions such as
{{ .vars.stage }} can be replaced with their values, while malformed or unresolved expressions
can cause template errors. A lone }} is ordinary text. Tag the body with
!literal to prevent unintended substitution and template errors and
run it exactly as written:
steps:
- name: report
type: script
interpreter: starlark
script: !literal |
row = {"name": "vpc", "status": "ok"}
print("{} is {}".format(row["name"], row["status"]))
print("Atmos templates look like {{ .vars.stage }}")
!literal works the same in custom commands, workflows, hooks, and in parallel and matrix
children. It applies to the field it tags: a literal script next to a templated interpreter
or env value still renders the other fields. It also works on interpreter, working_directory,
and individual env values. A script loaded with !include is still rendered as a template.
Atmos Automation Language
The Atmos Automation Language is a Python-like language for automating commands, workflows, and lifecycle hooks. Built on Starlark and embedded in Atmos, it provides component context, parallel execution, retries, and timeouts.
Set interpreter: starlark to run a script inside Atmos without installing another
interpreter. Use functions for reusable logic and steps.parallel for concurrent calls:
steps:
- name: calculate
type: script
interpreter: starlark
env:
REGION: us-east-1
script: |
def api():
return {"service": "api", "region": env["REGION"]}
def worker():
return {"service": "worker", "region": env["REGION"]}
output = steps.parallel(
functions = [api, worker],
max_concurrency = 2,
)
Pass function references (api), not calls (api()). The returned list follows
input order, even when branches finish in another order. Atmos waits for every
branch before returning.
Step output
Assign the top-level output global to set the step's value. A string is used as-is,
and any other value is JSON-encoded:
output = "abc" # {{ .steps.<name>.value }} is: abc
Or, for a structured value:
output = {"service": "api"} # {{ .steps.<name>.value }} is: {"service":"api"}
A value that cannot be encoded as JSON, such as a function, fails the step with the
error output` must be a string or JSON-encodable value: <reason>, followed by a hint to
assign a string, number, bool, list, or dict. Without output, the captured stdout
(everything passed to print()) is the step's value.
A later step reads a dict or list output as its JSON text, never as Go map syntax, so
{{ .steps.<name>.value }} renders {"service":"api"} for the dict above. JSON text
contains double quotes. When you place it inside a double-quoted shell string or a
Starlark string literal, the quotes end that string early and the shell strips them. Wrap the
template in single quotes in a shell command, or pass the value through env and read it
there:
steps:
- name: first
type: script
interpreter: starlark
script: |
output = {"n": 3}
- name: show
type: shell
command: echo '{{ .steps.first.value }}' # {"n":3}
- name: read
type: script
interpreter: starlark
env:
FIRST: "{{ .steps.first.value }}"
script: |
n = json.decode(env["FIRST"])["n"]
Atmos treats stdout as data and stderr as messages for people, so use print() for values
the step produces and ui.info(), ui.success(), or ui.warning() for status and progress
messages. ui.* output goes to stderr and never becomes the step's value.
Logging
Use log.trace(), log.debug(), log.info(), log.warn(), and log.error() for
diagnostics that matter when you troubleshoot a run but not when it succeeds. Each call
takes a message and optional keyword arguments, which are written as structured key/value
pairs through the Atmos logger. The logger honors --logs-level (or ATMOS_LOGS_LEVEL) and
logs.file, so a call below the configured level prints nothing. Output never goes to stdout
and never becomes the step's value, and secrets known to Atmos are masked.
| Function | Use it for | Where it goes |
|---|---|---|
print() | Data the step produces. | stdout. Becomes the step's value when output is unset. |
ui.info(), ui.success(), ui.warning() | Status and progress for people. | stderr, themed. |
log.*() | Diagnostics for troubleshooting. | The Atmos log, only at the configured log level. |
log.debug("resolved component", component="api", attempts=2)
log.warn("falling back to the default region", region="us-east-2")
$ atmos workflow deploy --logs-level=Debug
DEBU resolved component step=deploy component=api attempts=2
Every message includes a step field with the step name. Inside a steps.parallel group
with more than one task, a task field names the running task, for example task=grp/inner
for a nested group. A group with a single task adds no task field, just as it adds no
[task] output prefix. Field values
can be strings, numbers, booleans, or None; any other value is logged using its str()
form. Field names must be identifiers, and a field you pass named step or task replaces
the automatic one.
Language features
Top-level if, for, and while statements, set(), and recursion are available,
in the entry script and in loaded modules:
def fact(n):
return 1 if n <= 1 else n * fact(n - 1)
squares = []
for i in range(4):
squares.append(fact(i))
output = squares
Module-level names are assigned once. Rebinding a global, such as x = 1 followed
by x = 2, n += 1 at the top level, or assigning output in both branches of an
if/else, fails with cannot reassign global. Choose the value with a conditional
expression, or compute it inside a function:
# A conditional expression.
output = "blue" if env["REGION"] == "us-east-1" else "green"
# A function that returns the value.
def main():
if env["REGION"] == "us-east-1":
return "blue"
return "green"
output = main()
To carry changing state, mutate a dictionary or list instead of rebinding a name.
An indexed update such as state["n"] += 1 mutates the dictionary and keeps state bound to the same object.
Mutate it before handing it to steps.parallel, because everything reachable from a
parallel task becomes immutable at dispatch:
state = {"n": 0}
for name in ["api", "worker"]:
state["n"] += 1
def count():
return state["n"]
output = steps.parallel(functions = [count])
Parameterized tasks, retries, and timeouts
Use steps.task to describe work without executing it:
def deploy(name):
result = exec.run(["deployment-tool", "deploy", name])
return result.stdout
output = steps.parallel(
tasks = [
steps.task(
name = name,
function = deploy,
args = [name],
retry = {
"max_attempts": 3,
"initial_delay": "2s",
},
timeout = "5m",
)
for name in ["api", "worker", "scheduler"]
],
max_concurrency = 2,
fail_fast = True,
)
| API | Behavior |
|---|---|
steps.parallel(functions=..., tasks=..., max_concurrency=4, fail_fast=False) | Supply exactly one collection; empty collections return []. Nested groups each have their own limit. |
steps.task(name, function, args=[], kwargs={}, retry=None, timeout="") | Named deferred call. Names must be unique within a parallel group. |
exec.run(argv, working_directory=..., env={...}, output="stream", check=True) | Runs an argument list without a shell. Returns .stdout, .stderr, and .exit_code. By default, output streams live and failure raises an error. |
ui.info(message), ui.success(message), ui.warning(message) | Write through Atmos's UI channel. |
log.trace(message, **fields), log.debug(...), log.info(...), log.warn(...), log.error(...) | Write diagnostics to the Atmos logger, filtered by the configured log level. See Logging. |
json.encode(value), json.decode(text) | Convert structured values and JSON. |
fs.read_file(path) | Read local file contents as a string. Relative paths use the step working directory, including calls from loaded modules. Absolute paths are accepted. |
regex.search(pattern, text) | Return whether the pattern matches anywhere in the text. |
regex.findall(pattern, text) | Return complete matches in order, regardless of capture groups. |
regex.replace(pattern, replacement, text) | Replace every match with literal replacement text. |
Regular expressions use Go/RE2 syntax and support inline flags such as (?m).
Lookaround and pattern backreferences are unsupported. Replacement text is literal:
$1 is written as $1, without capture expansion. File reads use the same injectable
reader as module loading, allowing tests to supply file contents without disk access.
File writes and directory discovery are not available yet.
Both exec.run and component.exec take these options:
outputThe default,
"stream", shows the subprocess output live in the step output and also captures it. Use"capture"to capture stdout and stderr without showing them, which is useful when inspecting secret-bearing responses.checkThe default,
True, raises when the process exits nonzero; the error names the command and exit code and lists the last lines of stderr. PassFalseto get the result back and assert on.exit_code,.stdout, and.stderryourself.working_directory,envThe process directory and extra environment variables for this call.
A process that cannot start, such as a missing command or a nonexistent directory,
always raises, regardless of check. Cancellation, signals, and transport errors
also raise.
To test an expected command failure, pass check=False, then assert both its
exit code and its diagnostic:
def expect_timeout(argv):
result = exec.run(argv, check=False, output="capture")
if result.exit_code == 0:
fail("expected the command to fail")
if "deadline exceeded" not in result.stdout + result.stderr:
fail("command failed for an unexpected reason")
A handled nonzero exit does not itself trigger task retries. Call fail() when
the result should fail the current task and enter its retry policy.
Terminal output uses the shared Atmos I/O layer: print() writes to the step's
stdout channel, ui.* formats messages on its stderr channel, and subprocesses
stream through those same writers. Step output settings, masking, test capture,
and cast recording therefore apply. Captured values remain available to scripts;
masking applies when displaying them. File reads use the injectable filesystem
reader, independently of terminal output. There is no Starlark stdin/prompt API yet.
For example, a Starlark validator can read a recorded cast, decode its output events, and check the text without a Python interpreter:
def validate_cast(path):
lines = fs.read_file(path).splitlines()
events = [json.decode(line) for line in lines[1:] if line.strip()]
text = "".join([event[2] for event in events if event[1] in ["o", "e"]])
plain = regex.replace(r"\x1b\[[0-9;]*m", "", text)
if "All 3 component plans ready." not in plain:
fail("missing successful component plans")
validate_cast("release-plan.cast")
Retries repeat the entire failed function, including its earlier successful operations.
Successful sibling branches are not repeated. A task's timeout bounds the whole task,
including retries and delays. Retry fields use the Atmos retry configuration; the
conditions field for matching subprocess output does not apply to function tasks.
Set max_attempts or a timeout on every task that has a retry policy: a policy
without max_attempts keeps retrying until the task succeeds, its timeout expires, or
the workflow is canceled. Without a retry policy, a task runs once. A task that exceeds
its timeout fails with task "<name>" timed out after <duration>.
By default, all independent branches finish and failures are aggregated. With
fail_fast=True, a branch that exhausts its retries cancels running branches and
skips queued branches. The group still waits for cancellation to finish.
Before dispatch, Atmos freezes everything the tasks can reach: the task functions, their closures, default values, and arguments, and every global of each task function's module, including globals of loaded modules. A task's return value is frozen as well, once the task finishes. Create mutable lists and dictionaries inside each function and return results. Process directories and environment overrides belong to each invocation; they do not change Atmos's own working directory or environment.
Live output
Script output appears line by line as the script produces it, so a long-running
script shows progress while it runs. Inside a steps.parallel group with more than
one task, Atmos prefixes each line with the task name and keeps every line intact,
so output from concurrent tasks never interleaves mid-line:
[api] deploying api
[worker] deploying worker
[api] api ready
[worker] worker ready
Step labels
Like shell steps, a script step streams its output as-is. It does
not print a [name] label before the output or a ✓ name completed line after it. Set
output to choose a different output mode, or set show.labels: true
to print the labels:
steps:
- name: summarize
type: script
interpreter: starlark
show:
labels: true
script: |
print("done")
Loading files
Use the existing YAML include function for a script body:
steps:
- name: deploy
type: script
interpreter: starlark
working_directory: .
script: !include scripts/deploy.star
A script read from a local file knows where that file lives. Its load() calls resolve
relative to that file, whether the path is bare (lib/helpers.star) or starts with
./ or ../, and so do the load() calls of the modules it loads. Each kind of path
resolves like this:
| Path used in | Resolves against |
|---|---|
!include in a workflow manifest or a custom command | ./x and ../x: the directory of the file that contains the tag. x/y: the Atmos project base_path, including a --base-path flag or ATMOS_BASE_PATH override. Absolute paths are used as written. The result is the same wherever you run Atmos from. |
!include in a stack manifest hook (kind: step or kind: steps) | The same lookup as any stack manifest !include: relative to the manifest file, then to the project base_path. |
load() in a script read from a file (!include, !include.raw, or a standalone .star file) | The directory of that file. |
load() in an inline script: | body | The step's working_directory. |
fs.read_file and exec.run | The step's working_directory. |
With this project layout:
atmos.yaml # base_path: "./"
scripts/
deploy.star
lib/
helpers.star
stacks/workflows/deploy.yaml
the workflow includes the script by its path from the project base path, and the script loads its helper relative to its own file:
workflows:
deploy:
steps:
- name: deploy
type: script
interpreter: starlark
script: !include scripts/deploy.star
load("lib/helpers.star", "deploy")
output = deploy("api")
def deploy(name):
return {"service": name, "status": "ready"}
Tracebacks for a script read from a file name that file and the line within it,
relative to the project base path when available, for example scripts/deploy.star:3:5.
Files outside the project retain their absolute paths. If a Go template in the script emits extra lines, the
reported lines refer to the rendered script.
A hook script included from a stack manifest knows its file too. When a script that
has an interpreter sibling comes from a local !include or !include.raw file
(without a YQ expression, and never from a remote source), Atmos records the file in an
internal script_source key next to it, plus a script_source_sha256 fingerprint of the
included content:
components:
terraform:
mock:
hooks:
check:
kind: step
type: script
events: [before.terraform.plan]
with:
interpreter: starlark
script: !include scripts/hooks/check.star
atmos describe component and atmos describe stacks show the recorded provenance
under the hook, as the path relative to the project base path when the file is inside
the project (an absolute path otherwise):
hooks:
check:
with:
interpreter: starlark
script: "..."
script_source: scripts/hooks/check.star
script_source_sha256: 3f1c...e9a2
Atmos owns both keys: do not write them by hand. An include in the same mapping
replaces any value you set. The hook runner honors script_source only while the step's
script still hashes to script_source_sha256, so provenance cannot go stale. Stack
inheritance deep-merges maps: when a child stack replaces an included script with an
inline one, the merged hook still shows the base's script_source and fingerprint
in describe output, but the fingerprint no longer matches and Atmos ignores the
inherited source. The inline script resolves load() against the step's
working_directory, with no need to remove the inherited keys. A script that contains Go
templates still matches, because the fingerprint is checked against the script before
templates are rendered.
Modules are cached per invocation; cyclic loads fail with an error. Loaded functions
can be used in steps.parallel and steps.task.
Working directory and environment
The working_directory must exist; Atmos checks it before the script runs and fails
the step with a hint when it is missing. It applies to fs.read_file, exec.run, and
load() in inline scripts. A component.exec call defaults to the component directory instead.
The env dictionary contains only explicitly declared step inputs: the step's own env:
entries. In a custom command, command-level env: entries are not in env; they reach
child processes through the process environment instead. Child processes inherit the
effective execution environment, including authentication and command environment settings.
See Environment and templates for what Atmos renders.
Containers and dry runs
Embedded Starlark runs inside Atmos, so it cannot run in a container. A script step
under an enabled workflow or step container fails validation before the workflow
starts. Set container: false on the step:
workflows:
build:
container:
image: alpine:3.20
steps:
- name: plan
type: script
interpreter: starlark
container: false
script: !include scripts/plan.star
With atmos workflow --dry-run, Atmos parses each Starlark script step, so syntax
errors are reported, and runs nothing: no code, no processes. Script steps inside
parallel and matrix groups run nothing either.
Errors
A script failure is reported as a single-line message, with the Starlark traceback
shown below it. For example, fail("missing generated config") produces:
Error: workflow step execution failed: max attempts (1) exceeded, last error: starlark execution failed: fail:
missing generated config
Traceback (most recent call last):
s:1:5: in <toplevel>
Error in fail: fail: missing generated config
A failed exec.run adds the last lines of the command's stderr, and a task that
exceeds its timeout reads task "<name>" timed out after <duration>.
Custom components
This recording shows a Starlark custom command planning three custom application components using a loaded function, deferred tasks, and a concurrency limit of two. The command reads resolved stack configuration without changing infrastructure.
Resolve a component with components.get(name, stack, type). In workflows and
hooks, pass the stack explicitly. A custom command that declares a component:
already has its component and stack in scope as ctx.component:
commands:
- name: deploy-peers
arguments:
- name: component
provides: component
required: true
flags:
- name: stack
shorthand: s
provides: stack
required: true
component:
type: application
steps:
- name: deploy
type: script
interpreter: starlark
script: |
stack = ctx.component.stack
def deploy(name, stack):
peer = components.get(
name = name,
stack = stack,
type = "application",
)
return peer.exec(["./deploy"]).stdout
output = steps.parallel(
tasks = [
steps.task(name=n, function=deploy, args=[n, stack], timeout="5m")
for n in ["api", "worker"]
],
max_concurrency = 2,
)
Where ctx.component is set
The execution context decides what ctx.component holds:
| Context | ctx.component |
|---|---|
Custom command with component: | The command's component. Atmos resolves it on first access, so scripts that never use it never pay for the lookup. |
| Hook | The hook's component, plus ctx.hook and ctx.operation. |
| Workflow | None, because no component is in scope. |
parallel and matrix children inherit ctx.component and components.get from the
step that contains them. Atmos resolves each components.get result once per run and
reuses it, including across parallel tasks.
Lazy ctx.component attribute access uses the invocation deadline, because the
Starlark attribute interface does not receive the calling task. To bound component
resolution by a task timeout, call components.get inside that task with explicit
name, stack, and type values. A caller waiting for another task to resolve the same
component can cancel independently. Read any ctx.component reference fields before
starting parallel tasks; accessing them inside the task also triggers lazy resolution.
In a workflow, ctx.component is None, so a script that reads ctx.component.stack
fails with NoneType has no .stack field or method. Name the stack explicitly instead:
workflows:
deploy-peers:
steps:
- name: deploy
type: script
interpreter: starlark
script: |
def deploy(name):
peer = components.get(name = name, stack = "dev", type = "application")
return peer.exec(["./deploy"]).stdout
output = steps.parallel(
tasks = [
steps.task(name=n, function=deploy, args=[n], timeout="5m")
for n in ["api", "worker"]
],
max_concurrency = 2,
)
Handles expose name, type, stack, implementation, path, vars, settings,
metadata, env, and config. Their configuration is resolved through Atmos's
inheritance and YAML-function pipeline and is read-only. The logical name can
differ from the implementation directory when components inherit shared code.
The component.exec function accepts the same arguments as exec.run and defaults to
the component directory. It inherits the invocation environment, overlays component
env, preserves custom-command and explicit step env overrides, and finally applies
per-call env overrides. Component lookups are serialized; the parallel runner
executes independent processes concurrently.
Arbitrary typed-step dispatch and Starlark test-file discovery are not available yet.
Standalone executable scripts
Use an Atmos shebang to run an automation program directly:
#!/usr/bin/env atmos
dependencies.tools("jqlang/jq", "1.7.1")
ui.info("Script arguments: %s" % ctx.args)
exec.run(["jq", "--version"])
After chmod +x deploy.star, run ./deploy.star dev, or use
atmos ./deploy.star dev. A leading .star file or an explicit path with an Atmos
shebang selects script execution. No arguments preserve the default CLI screen;
bare extensionless names remain CLI commands. Script arguments such as --help
belong to the script and are exposed through immutable ctx.args.
ctx.script.path and ctx.script.directory identify the script file; steps whose script
comes from a local !include set them too. Local load() paths resolve against the
script file; commands retain the caller's working directory.
Each dependencies.tools(name, version) call installs a missing tool through the
existing dependency manager and updates PATH for subsequent process calls in this
invocation. Declare tools before parallel tasks; declarations inside branches are
rejected. Repeated identical pins are cached; conflicting versions fail. Single-file scripts can themselves be packaged
as type: http, format: raw toolchain entries and installed with
atmos toolchain install owner/tool@version. The Atmos interpreter must be on PATH.
Pass the script filename directly to atmos, before any script arguments. Global
Atmos options before the script filename are not supported.
Use atmos.toolchain("install", "jq@1.7.1") for an explicit toolchain CLI operation.
It returns stdout, stderr, and exit_code, with the same output and error
policies as atmos.run. Optional flags={...} and args=[...] pass CLI options.
This operation does not change the calling script's PATH; use dependencies.tools
when subsequent script calls need the installed tool.
Declaring a standalone command
Standalone scripts can declare their interface using Atmos's native flag handling:
#!/usr/bin/env atmos
def validate(args, flags):
if flags["replicas"] < 1:
fail("replicas must be positive")
def main(args, flags):
print("{}: {} replicas".format(args["service"], flags["replicas"]))
cli.command(
description = "Report a service's replica count",
args = [cli.arg("service", description="Service name")],
flags = [cli.flag("replicas", type="int", shorthand="r", default=2)],
validate = validate,
run = main,
)
After saving this as replicas.star and making it executable, run
./replicas.star api -r 3 or ./replicas.star --help. Help skips both callbacks;
parse failures and failed validation prevent main from running. Keep work with
side effects inside main, since ordinary top-level statements still execute
when the script loads.
| Declaration | Options |
|---|---|
cli.arg(name, ...) | description, required (default True). Required arguments precede optional ones. Omitted optional arguments are None. |
cli.flag(name, ...) | type ("string", "int", "bool", or "string_list"), default, shorthand, description, required, choices, and an optional env binding. |
cli.command(run=main, ...) | name (defaults to the script filename), description, args, flags, and an optional validate callback. Call once per standalone invocation, outside parallel tasks. |
main(args, flags) and validate(args, flags) receive immutable dictionaries.
Names are dictionary keys, including hyphenated flags. Integers and booleans arrive
as typed values; string_list flags accept comma-separated or repeated values.
choices applies to string and string-list flags. A required flag must be supplied
on the command line or through its declared environment binding; it cannot also
declare a default. Boolean flags use defaults and cannot be required.
Command-line values override environment bindings, which override defaults. The
environment binding is optional; scripts always read the parsed flags dictionary
directly. validate may call fail() with an explanation, return False to reject
the input, or return None or True to accept it. The value returned by main
is the return value of cli.command; assign it to output to emit structured data.
Unknown flags and invalid values fail through the native parser. -- ends flag
parsing so later values are positional arguments. All arguments after the script
filename belong to the script, including flags such as --config and --help.
Scripts that do not use cli.command continue to receive the original ctx.args.
Reading command inputs in script steps
Custom-command script steps read parsed inputs directly through ctx.flags and
ctx.arguments, without templates or environment mappings:
replicas = int(ctx.flags["replicas"])
service = ctx.arguments["service"]
print("{}: {} replicas".format(service, replicas))
These dictionaries preserve the host command's value types. A custom command flag
declared as type: string still needs int(...) when used in arithmetic, while a
type: int flag arrives as an integer and a type: bool flag as a boolean. Positional
arguments are always strings. An optional argument that has no default and was not
supplied is an empty string, and an omitted argument with a default holds its default.
Workflow flags are strings, and ctx.flags in a workflow always includes stack (an
empty string when no stack applies). Inputs are immutable and are inherited by control and
test children. Contexts without declared inputs expose empty dictionaries. Standalone
command callbacks receive their parsed values as the args and flags callback
parameters.
Calling Atmos commands
Built-in commands are callable directly by name. Pass CLI positional arguments as
strings, options through flags, and additional literal arguments through args:
atmos.vendor("pull", flags={"component": "vpc", "stack": "dev"})
atmos.scaffold("generate", "service-template", "services/api", flags={"dry-run": True})
components_json = atmos.list("components", flags={"stack": "dev", "format": "json"}, output="capture")
component = atmos.describe("component", "api", flags={"stack": "dev"}, output="capture")
atmos.config("get", args=["base_path"])
atmos.version()
Generic wrappers are available for about, ai, ansible, atlantis, auth,
aws, azure, cast, ci, completion, composition, config, container,
describe, devcontainer, docs, emulator, env, gcp, git, helmfile,
help, init, kubernetes, list, lsp, mcp, packer, pro, profile,
sbom, scaffold, secret, stack, store, support, theme, validate,
vendor, version, and workflow. These wrappers retain each command's native
behavior, including authentication and experimental notices. They return process
results; decode JSON stdout with json.decode when structured values are needed.
Use atmos.toolchain(command, tool="", flags={}, args=[]) for tool operations.
Use atmos.run(argv) for any Atmos command, including custom commands. Use
atmos.terraform(command, component=..., stack=...) and
atmos.helm(command, component=..., stack=...) for component operations:
plan = atmos.terraform(
"plan",
component = "vpc",
stack = "dev",
flags = {"detailed-exitcode": True},
)
validation = atmos.run([
"casts", "validate", "demo", "fixtures", "starlark", "release-plan",
])
These calls invoke the current Atmos executable through the same process runner
as exec.run. Arguments are passed directly without shell parsing. These
functions accept working_directory, env, output="stream", and check=True, and
return .stdout, .stderr, and .exit_code. Failures raise unless check=False;
launch, cancellation, signal, and transport errors always raise. A Terraform plan
with flags={"detailed-exitcode": True} accepts exit code 2 as changes detected.
The generic atmos.run uses ordinary exit-code handling.
Command wrappers and component helpers accept a flags dictionary and an args list. Bare flag names
become --name; use an explicit prefix for native tool flags, such as "-var".
The detailed-exitcode flag automatically uses Terraform's single-dash spelling. Values
may be strings, integers, booleans, or lists to repeat a flag. A value of True emits
the flag, and False emits =false. Spaces and shell metacharacters remain literal
argument text.
Atmos calls default to the invocation directory, preserving configuration discovery
even when a script runs in a component directory. An explicit relative
working_directory is relative to that invocation directory. The child inherits
the effective process environment; parent CLI-only flags are not automatically
replayed. Pass extra CLI options through args or atmos.run when needed.
Atmos calls work inside ordinary functions passed to steps.parallel. Apply retries
and timeouts using steps.task; retries repeat the function containing the call.
Calls trigger the normal command lifecycle, including hooks. When invoking a command
from a hook, use event scoping or the command's supported hook-skipping options to
avoid recursively invoking the same hook.
Lifecycle hook context
Starlark scripts work in kind: step, kind: steps, and type: test hooks.
They receive read-only lifecycle context, including in parallel functions:
| Object | Fields |
|---|---|
ctx.component | name, type, stack, implementation, path, vars, settings, metadata, env, config, and exec |
ctx.hook | name and canonical event, such as after.terraform.apply |
ctx.operation | command, status, exit_code, error, stdout, stderr |
The component contains the hook runner's execution snapshot, including inherited
settings. It does not re-resolve the stack or fetch Terraform outputs. Its path
matches the hook runner's component working directory, including provisioned workdirs.
Use dictionary access for settings and vars. To look up a different component, call
components.get(name, stack, type) with an explicit stack, just as in a workflow.
For before hooks, operation status, exit code, and error are None. After hooks
expose the supplied outcome; an absent error is None. The command is the canonical
operation, such as terraform apply. These fields describe the parent operation,
not processes launched by the hook. Ordinary lifecycle hooks do not capture parent
stdout/stderr, so those fields are None. Terraform state and outputs are not
automatically attached.
Outside hooks, ctx.hook and ctx.operation are None. A hook without an individual
component binding, including aggregate hooks, has ctx.component == None.
For example, configure a reusable smoke test through inherited settings:
settings:
post_apply:
enabled: true
command: ["./smoke-test", "--strict"]
hooks:
smoke-test:
events: [after.terraform.apply]
kind: step
type: script
on_failure: fail
with:
interpreter: starlark
script: !include scripts/post-apply.star
def post_apply():
policy = ctx.component.settings.get("post_apply", {})
if not policy.get("enabled", False):
return
if ctx.operation.status != "success":
return
ctx.component.exec(policy.get("command", ["./smoke-test"]))
ui.success("Smoke tests passed.")
post_apply()
The same context is available when a hook runs Starlark checks through a type: test
step:
hooks:
verify:
events: [after.terraform.apply]
kind: step
type: test
on_failure: fail
with:
steps:
- name: component is tagged
type: script
interpreter: starlark
script: |
if "owner" not in ctx.component.vars.get("tags", {}):
fail("missing owner tag on " + ctx.component.name)
Secrets, stores, and error reporting
Direct Starlark APIs for secret access, store reads/writes, and Terraform state/output reads are not available yet. Scripts can consume values resolved by existing YAML functions through explicit inputs or component configuration.
Use errors.build to create script errors
with the same explanations, hints, examples, context, and exit codes as Atmos's
own errors. Chain the builder methods and call .fail() to raise the error.
Unhandled Starlark errors propagate through Atmos's normal command error path, including Sentry capture when configured. Handled failures and retries that eventually succeed do not produce a final parent-command error. An Atmos subprocess can still report its own errors independently. Dedicated Starlark stack frames and parallel-task metadata are not yet mapped into Sentry events.