An entity type declares what its blocks may contain: attributes that take a
value, and nested blocks that group values. In Go each is a struct field with
an xcl tag; in Python each is a dataclass field whose annotation gives its
shape. This page walks through every shape with one entity type, an app,
adding a field for each.
Required attributes
app "web" { image = "nginx:1.27"}An attribute with no option is required: a block that leaves it out is an error at the block’s position.
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"`}@dataclassclass App: image: str name: str = xcl.field(label=True, default="")Optional attributes
app "web" { image = "nginx:1.27" replicas = 3}The optional option lets a block leave the attribute out. In Python a field
with a default, or annotated | None, is optional. The tag carries no
default: the Python dataclass default is the value an omitted attribute gets,
and in Go the application decides what an omitted replicas means. An
omitted attribute held as a Go pointer is nil, and one annotated | None
in Python is None, rather than an empty value such as 0 or "".
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Replicas int `xcl:"replicas,optional" json:"replicas,omitempty"`}@dataclassclass App: image: str replicas: int = 2 name: str = xcl.field(label=True, default="")Nested blocks
app "web" { image = "nginx:1.27"
health { path = "/healthz" interval = 10 }}A field whose type is a struct, tagged block, is a nested block. A pointer
makes the block optional; a plain struct makes it required, so a block that
leaves it out is an error at the block’s position. The nested type is not an
entity: it is not registered as a block type and has no address of its own.
In Python a field annotated with a dataclass is a block, and | None with a
default of None makes it optional.
type Health struct { Path string `xcl:"path" json:"path"` Interval int `xcl:"interval,optional" json:"interval,omitempty"`}
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Health *Health `xcl:"health,block" json:"health,omitempty"`}@dataclassclass Health: path: str interval: int = 10
@dataclassclass App: image: str health: Health | None = None name: str = xcl.field(label=True, default="")How deep blocks nest
layout "main" { column { width = "60"
row { height = "50"
column { width = "30" } } }}A nested block may hold blocks of its own, including blocks of its own type, directly or through others: a column holds rows, and a row holds columns. Configuration nests blocks at most ten deep within an entity, a block’s own blocks being one deep. A block nested deeper is an error at that block:
column is nested 11 blocks deep, and blocks nest at most 10 deeptype Column struct { Width string `xcl:"width,optional" json:"width,omitempty"` Rows []Row `xcl:"row,block" json:"row,omitempty"`}
type Row struct { Height string `xcl:"height,optional" json:"height,omitempty"` Columns []Column `xcl:"column,block" json:"column,omitempty"`}
type Layout struct { Name string `xcl:"name,label" json:"name"` Columns []Column `xcl:"column,block" json:"column,omitempty"`}Lists of blocks
app "web" { image = "nginx:1.27"
volume "data" { source = "/var/lib/web" }
volume { source = "/tmp/cache" }}A slice of structs tagged block takes the block any number of times, and
the order the blocks are written in is the order of the slice. An element
type may declare a field tagged label, and then a block may carry a name
after the keyword, volume "data" {}. The name is not required: named and
unnamed blocks may mix in one list, and an unnamed block leaves the field
empty. Read an element by position, app.web.volume[0].source, or all of
them with a splat, app.web.volume[*].source; see
Expressions.
type Volume struct { Name string `xcl:"name,label" json:"name,omitempty"` Source string `xcl:"source" json:"source"`}
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Volumes []Volume `xcl:"volume,block" json:"volumes,omitempty"`}@dataclassclass Volume: source: str name: str = xcl.field(label=True, default="")
@dataclassclass App: image: str volume: list[Volume] = xcl.field(default_factory=list) name: str = xcl.field(label=True, default="")Named blocks collected by name
app "web" { image = "nginx:1.27"
listener "http" { port = 80 }
listener "https" { port = 443 tls = true }}A map of structs tagged block, whose element type has a label field,
collects the blocks by their name. Each block must carry a name, and each
name is used once. Another entity reaches one by its name,
app.web.listener.https.port, and iterates them with a for expression,
{ for name, l in app.web.listener : name => l.port }.
A block without a name is an error at the block: listener blocks are collected by name. A second block with a name already used is an error at
the second block: Duplicate listener name: A listener named "http" was already defined at <file>:<line>,<col>-<col>.
type Listener struct { Name string `xcl:"name,label" json:"name"` Port int `xcl:"port" json:"port"` TLS bool `xcl:"tls,optional" json:"tls,omitempty"`}
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Listeners map[string]Listener `xcl:"listener,block" json:"listeners,omitempty"`}@dataclassclass Listener: port: int tls: bool = False name: str = xcl.field(label=True, default="")
@dataclassclass App: image: str listener: dict[str, Listener] = xcl.field(default_factory=dict) name: str = xcl.field(label=True, default="")Map attributes
app "web" { image = "nginx:1.27"
labels = { team = "platform" tier = "frontend" }}A map[string]string field, or any map of plain values, is an attribute
that takes a map value. It is supported, and fits values that really are a
flat set of keys, such as labels. For anything with structure, named blocks
are the encouraged style: a map value has no labels, each entry is a single
value rather than a block of its own, and a map has no order.
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Labels map[string]string `xcl:"labels,optional" json:"labels,omitempty"`}@dataclassclass App: image: str labels: dict[str, str] = xcl.field(default_factory=dict) name: str = xcl.field(label=True, default="")Object values
app "web" { image = "nginx:1.27"
default_endpoint = { port = 80, tls = true }}A struct field without the block option is an attribute that takes an
object value, written with = and braces, rather than a nested block. The
object’s fields are declared like any other: a field tagged optional may be
left out of the object. In Python a dataclass-typed field is a block by
default, and xcl.field(block=False) makes it an object attribute instead.
type Endpoint struct { Port int `xcl:"port" json:"port"` TLS bool `xcl:"tls,optional" json:"tls,omitempty"`}
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` DefaultEndpoint Endpoint `xcl:"default_endpoint,optional" json:"default_endpoint,omitempty"`}@dataclassclass Endpoint: port: int tls: bool = False
@dataclassclass App: image: str default_endpoint: Endpoint | None = xcl.field(block=False, default=None) name: str = xcl.field(label=True, default="")Attributes of any value
app "web" { image = "nginx:1.27"
settings = { retries = 3 region = "eu" }}A field of type any takes whatever the attribute’s expression evaluates to,
as plain data: an object becomes a map[string]any, a list a []any, a number
a float64, alongside strings and bools. Another block can read it like any
attribute, app.web.settings.region.
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Settings any `xcl:"settings,optional" json:"settings,omitempty"`}Computed fields
app "web" { image = "nginx:1.27"}
output "url" { value = app.web.endpoint}A field with the computed option belongs to the provider: the provider
sets it when the entity is applied, and other blocks can read it. A block
that sets a computed field in configuration is an error at that attribute.
In Python the annotation xcl.Computed[T] marks the field.
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Endpoint string `xcl:"endpoint,optional,computed" json:"endpoint,omitempty"`}@dataclassclass App: image: str endpoint: xcl.Computed[str] = None name: str = xcl.field(label=True, default="")Sensitive fields
app "web" { image = "nginx:1.27" token = env("WEB_TOKEN")}A field of type types.Sensitive[T] is set like a plain field but shown as
(sensitive) everywhere xcl and the application print it; the real value is
reached only with Reveal(). In Python the annotation is
xcl.Sensitive[T], and a sensitive value looked up through cfg.find is
redacted, so reveal() returns None. See Sensitive values.
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Token types.Sensitive[string] `xcl:"token,optional" json:"token,omitempty"`}@dataclassclass App: image: str token: xcl.Sensitive[str] | None = None name: str = xcl.field(label=True, default="")The entity base
Every entity has the same base, depends_on, disabled and meta, which xcl
holds for it beside the type’s own fields. In Go a type is a plain struct: a
field tagged as a label, such as xcl:"name,label", is filled from the
block’s labels, and a field called Meta, a types.Meta with no tag,
receives a copy of the metadata, with Meta.Disabled and Meta.DependsOn
among it. A type declares no fields called depends_on, disabled or
meta, and does not embed types.ResourceBase, which xcl refuses when the
type is registered. In Python a type is a plain dataclass in the same way: a
field declared xcl.field(label=True) is filled from a label, a field
meta: xcl.Meta | None receives the metadata, and fields called disabled
and depends_on receive those values when the class declares them. See
Built-in fields. Put together, the
app type this page has built is:
type App struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Replicas int `xcl:"replicas,optional" json:"replicas,omitempty"` Health *Health `xcl:"health,block" json:"health,omitempty"` Volumes []Volume `xcl:"volume,block" json:"volumes,omitempty"` Listeners map[string]Listener `xcl:"listener,block" json:"listeners,omitempty"` Labels map[string]string `xcl:"labels,optional" json:"labels,omitempty"` DefaultEndpoint Endpoint `xcl:"default_endpoint,optional" json:"default_endpoint,omitempty"` Endpoint string `xcl:"endpoint,optional,computed" json:"endpoint,omitempty"` Token types.Sensitive[string] `xcl:"token,optional" json:"token,omitempty"`}@dataclassclass App: image: str replicas: int = 2 health: Health | None = None volume: list[Volume] = xcl.field(default_factory=list) listener: dict[str, Listener] = xcl.field(default_factory=dict) labels: dict[str, str] = xcl.field(default_factory=dict) default_endpoint: Endpoint | None = xcl.field(block=False, default=None) endpoint: xcl.Computed[str] = None token: xcl.Sensitive[str] | None = None name: str = xcl.field(label=True, default="")Types that share fields share them through a struct of their own. In Go, put
the common fields in a struct and embed it in each type with
xcl:",remain"; its fields are read as the type’s own, its label fields and
Meta field included. In Python, put them in a dataclass and subclass it;
the base must itself be a dataclass, and only the subclasses are registered.
A subclass that adds a required field after the base’s defaulted ones is
declared @dataclass(kw_only=True), as the dataclass rules require.
type Workload struct { Name string `xcl:"name,label" json:"name"`
Image string `xcl:"image" json:"image"` Replicas int `xcl:"replicas,optional" json:"replicas,omitempty"`}
type App struct { Workload `xcl:",remain"`
Listeners map[string]Listener `xcl:"listener,block" json:"listeners,omitempty"`}
type Job struct { Workload `xcl:",remain"`
Schedule string `xcl:"schedule" json:"schedule"`}@dataclassclass Workload: image: str replicas: int = 2 name: str = xcl.field(label=True, default="")
@dataclassclass App(Workload): listener: dict[str, Listener] = xcl.field(default_factory=dict)
@dataclass(kw_only=True)class Job(Workload): schedule: str
local.register_type(App, "app")local.register_type(Job, "job")Next
References and processing order covers how one entity’s attributes read another’s, and the order that gives. For the sensitive field, see Sensitive values; for how an entity’s text is printed back, see Configuration text.