Skip to main content

Script namespaces, records, and typed configuration

· 3 min read
Erik Osterman
Founder @ Cloud Posse

Atmos automation scripts can now import a library as a namespace:

kube = module.load("lib/kube.star")
kube.get("pods")

The library defines its functions directly. There is no export object to maintain and no need to repeat each function name in an import list.

This release also adds struct() records and typed YAML/JSON configuration with inline or reusable schemas, source-line diagnostics, and immutable deep merging.

Share functions between scripts​

Create lib/kube.star:

def get(resource):
return exec.run(["kubectl", "get", resource])

module.load exposes the file's public functions and constants. Names beginning with _ remain private. Relative paths resolve beside the calling script, including calls inside loaded functions; inline scripts use their working directory.

The function works inside conditionals and parallel tasks. Successful loads share a cache with ordinary load(), so concurrent callers initialize a file once. Cyclic imports fail instead of waiting indefinitely, and canceling a waiting task does not cancel another task's initialization. Failed initialization can be retried, so top-level side effects should be repeatable.

Loaded globals are frozen. Library functions can create local collections, but cannot modify shared global lists or dictionaries after initialization.

Create records with named fields​

The new struct() constructor groups values for dot access:

service = struct(name="api", port=8080)
print(service.name)
output = service

Fields cannot be reassigned. Nested collections remain mutable until module loading or parallel execution freezes them. Data-only records encode as JSON objects; records containing functions cannot be JSON-encoded.

Load typed configuration​

config.load reads YAML or JSON with inline schemas:

cfg = config.load("app.yaml", schema={
"database": {
"host": str,
"port": schema.field(int, default=5432, min=1, max=65535),
},
"debug": schema.field(bool, default=False),
})
print(cfg.database.port)

Reuse a JSON Schema file or an HTTPS schema:

cfg = config.load("app.yaml", schema="app.schema.json")
cfg = config.load("app.yaml", schema="https://example.com/schemas/app.schema.json")

Schemas can be JSON or YAML, from local files or HTTPS URLs; relative $ref documents resolve beside the schema. Inline schemas apply explicit schema.field defaults, reject unknown fields, and support lists, enums, and optional values. File schemas follow standard JSON Schema semantics, including annotation-only defaults.

config.validate checks existing values. Results are immutable copies with dot and index access. File validation errors include line and column numbers in YAML and JSON, plus the failing field path, for example:

app.yaml:2:9: database.port: expected int, got string

Both APIs work inside loaded functions and parallel tasks. HTTPS schemas support relative references, with cancellation, request timeouts, and bounded downloads. Remote schemas cannot reference local files.

Combine configuration​

Records support Atmos deep merging:

base = config.load("base.yaml", schema=app_schema)
overrides = config.load("prod.yaml", schema=override_schema)
cfg = base.merge(overrides)
cfg_with_tags = base.merge(overrides, list_merge_strategy="append")

Nested objects merge recursively and right-hand values take precedence. Lists replace by default; append and Atmos's merge by index are also available. The result is a new frozen record; both inputs remain unchanged. To enforce a schema on combined data, use config.validate(base.merge(overrides), schema=app_schema).

These APIs are available in standalone scripts, automation script steps, hooks, and their loaded helpers. YAML !starlark expressions and policy predicates keep their existing environments.

See the module loader reference, struct reference, configuration loading reference, and inline schema helpers for the complete behavior.