Loops & Register
glidesh has two loop mechanisms: step loops that repeat an entire step, and template loops that generate repeated blocks inside template files.
Register
Section titled “Register”Use register="var_name" on any step to capture the module’s output into a variable for later use:
step "Get disk list" { shell "lsblk -dn -o NAME" register="disk_names"}
step "Show disks" { shell "echo 'Found disks: ${disk_names}'"}The output is trimmed of leading/trailing whitespace before being stored.
Step Loops
Section titled “Step Loops”Use loop="${var_name}" on a step to iterate over newline-separated values. Each iteration injects the current value as ${item}:
step "List disks" { shell "lsblk -dn -o NAME" register="disks"}
step "Format each disk" loop="${disks}" { disk "/dev/${item}" { fs "ext4" mount "/mnt/${item}" }}Chaining Register and Loop
Section titled “Chaining Register and Loop”Register and loop compose naturally for dynamic infrastructure tasks:
step "Find config files" { shell "ls /etc/myapp/conf.d/*.conf" register="config_files"}
step "Validate each config" loop="${config_files}" { shell "myapp validate --config ${item}"}Looping over structured variables
Section titled “Looping over structured variables”A step loop can also iterate over a structured variable — a list of named-field maps defined in your plan’s vars block. Each iteration binds the row’s fields as ${item.<field>} (instead of a single ${item} value):
plan "provision-vms" { vars { vms { - name="web" port="2301" - name="db" port="2302" } }
step "Create each VM" loop="${vms}" { shell "incus launch images:ubuntu/22.04 ${item.name} --config limits.cpu=2" }
step "Wait for cloud-init" loop="${vms}" { shell "incus exec ${item.name} -- cloud-init status --wait" { success_codes "0,2" retries 30 delay 5 } }}Each ${item.<field>} resolves to the value of that field for the current row. Reference any field defined on the collection’s - nodes (${item.name}, ${item.port}, …). This is the per-step counterpart to template loops, which expand the same structured data inside a file.
A loop source that is a plain newline-separated string binds
${item}; one that names a structured collection binds${item.<field>}. If the named variable is neither, the step fails withLoop variable '<name>' is not defined.
Template Loops
Section titled “Template Loops”Template loops generate repeated content inside a template file. This is different from step loops — instead of repeating an entire step, you repeat lines within a file.
Use ${for <binding> in <collection>}...${endfor} syntax in any file with template=#true.
Defining the data
Section titled “Defining the data”Define structured variables in your plan’s vars block:
plan "setup-proxy" { vars { api-keys { - name="alice" key="sk-aaa" - name="bob" key="sk-bbb" - name="charlie" key="sk-ccc" } }
step "Deploy config" { file "/etc/proxy/config.conf" src="templates/config.conf" template=#true }}Using the loop in a template
Section titled “Using the loop in a template”Inside templates/config.conf:
# API key mapping${for k in api-keys}key "${k.name}" = "${k.key}"${endfor}This renders to:
# API key mapping
key "alice" = "sk-aaa"
key "bob" = "sk-bbb"
key "charlie" = "sk-ccc"Each item in the collection is a map of named fields. Access fields with dot notation: ${k.name}, ${k.key}, etc. The binding name (k in this example) is your choice.
Separator
Section titled “Separator”When generating JSON, CSV, or similar formats, you often need a separator between items but not after the last one. Use the optional separator="..." parameter:
[${for item in items separator=","} {"name": "${item.name}", "key": "${item.key}"}${endfor}]With two items, this renders to:
[ {"name": "alice", "key": "sk-aaa"}, {"name": "bob", "key": "sk-bbb"}]The separator is inserted between items only — not before the first or after the last. If the collection has zero or one items, no separator is output.
Looping over inventory groups
Section titled “Looping over inventory groups”You can also loop over all hosts in an inventory group using @group.<name>:
group "backend" { host "api-1" "10.0.1.10" user="deploy" host "api-2" "10.0.1.11" user="deploy" host "api-3" "10.0.1.12" user="deploy"}Inside a template file:
upstream backend { ${for h in @group.backend} server ${h.address}:8080; ${endfor}}Renders to:
upstream backend {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
}Each host in the group exposes these fields: name, address, user, port.
Combining loops with inventory references
Section titled “Combining loops with inventory references”Template loops and inventory references work together. Here’s a complete example — a Caddyfile that maps API keys to developers and proxies to another host:
group "services" { host "caddy" "10.0.1.1" user="deploy" host "bifrost" "10.0.1.5" user="app" { vars { api-port "8080" } }}plan "setup-caddy" { vars { api-keys { - name="k1" value="sk-k-asd" - name="k2" value="sk-k-das" } }
step "Deploy Caddyfile" { file "/etc/caddy/Caddyfile" src="templates/Caddyfile" template=#true }}templates/Caddyfile:
llm.example.com { map {http.request.header.Authorization} {developer_from_bearer} { ${for key in api-keys} ~Bearer\s+${key.value} "${key.name}" ${endfor} default "" }
handle { reverse_proxy ${@inventory.bifrost.address}:${@inventory.bifrost.vars.api-port} }}Step Loop Example
Section titled “Step Loop Example”plan "disk-setup" { step "Discover available disks" { shell "lsblk -dn -o NAME | grep -v sda" register="available_disks" }
step "Format and mount each disk" loop="${available_disks}" { disk "/dev/${item}" { fs "ext4" mount "/mnt/${item}" opts "defaults,noatime" } }
step "Verify mounts" { shell "df -h | grep /mnt" }}See the disk-management example for a complete working example.