zester

Filters

Filters transform template values. They are applied with the pipe (|) operator and can be chained left to right. Zester registers custom filters on top of the full set of built-in Gonja/Jinja2 filters.

{{ value | filter_name }}
{{ value | filter_name(arg) }}
{{ value | upper | truncate(10) }}

Zester Filters

These filters are registered in pkg/template/filters.go and are specific to Zester.

settings_decrypt

Placeholder for template-side decryption. During master-side rendering, this filter passes through the value unchanged. Actual decryption happens on the peel side after settings are delivered via NATS KV.

Referencing an encrypted value in a state file
database:
  password: {{ settings.database.password | settings_decrypt }}

Master-side behavior

settings_decrypt is a no-op during template rendering on the master. It exists so templates can express decryption intent. The peel decrypts ENC[nkey,...] values automatically when it receives settings. See Settings Encryption for the full flow.


yaml_encode

Converts a value to a YAML-compatible string representation. Useful when embedding structured data inline in YAML output.

Conversion rules:

Input TypeOutput
nil"null"
stringPassed through unchanged
bool"true" or "false"
int, int64, float64String form (e.g., "42", "3.14")
Complex types (maps, lists)JSON-marshaled string (valid YAML)
Encoding values for YAML output
# String passes through
region: {{ "us-east-1" | yaml_encode }}
# Output: region: us-east-1

# Boolean becomes string
debug: {{ true | yaml_encode }}
# Output: debug: true

# Number becomes string
port: {{ 8080 | yaml_encode }}
# Output: port: 8080

# Complex type becomes JSON (which is valid YAML)
labels: {{ {"app": "nginx", "env": "prod"} | yaml_encode }}
# Output: labels: {"app":"nginx","env":"prod"}

JSON is valid YAML

Since JSON is a subset of YAML, the JSON-marshaled output for complex types is always valid YAML. This makes yaml_encode safe for embedding maps and lists in YAML configuration.


to_json

Serializes any value to a JSON string. Returns "null" for nil values and an empty string on marshaling errors.

Serializing values to JSON
# Simple values
config_json: {{ settings.app | to_json }}

# Embedding facts as JSON
metadata: '{{ facts | to_json }}'

# In a file.managed content block
monitoring_config:
  file.managed:
    - name: /etc/monitor/config.json
    - contents: {{ settings.monitoring | to_json }}
Nil handling
# nil values become "null"
optional: {{ none | to_json }}
# Output: optional: null

regex_match

Checks if the input string contains a pattern. Returns true or false.

Usage: {{ value | regex_match("pattern") }}

Pattern matching on facts
{% if facts.os.name | regex_match("ubuntu") %}
package_manager: apt
{% endif %}

{% if facts.network.fqdn | regex_match(".prod.") %}
environment: production
{% else %}
environment: development
{% endif %}

Not actual regex

Despite the name, regex_match currently uses substring matching (strings.Contains), not regular expressions. The pattern "prod" will match any string containing "prod" -- such as "production", "prod-01", or "reproduce". Plan your patterns accordingly.


length (extended)

The built-in length filter is extended in Zester to support the MutableList type returned by mlist(). Gonja's default length filter does not recognize pointer-to-struct types and returns 0 for them; Zester replaces it with a version that checks for a Len() int method first, then falls back to the standard behavior for slices, maps, and strings.

This means {{ mlist_var | length }} works as expected alongside {{ plain_list | length }} — no special handling is needed in templates.

length works for both plain lists and mutable lists
{% set peers = basket("role:api", "network.ipv4") %}
peer_count: {{ peers | length }}

{% set servers = mlist() %}
{% do servers.Append("10.0.0.1") %}
server_count: {{ servers | length }}

Source: pkg/template/filters.go


dict_merge

Merges a dictionary into the input dictionary. Override values take precedence over base values.

Usage: {{ base_dict | dict_merge(override_dict) }}

Merging configuration dictionaries
{% set defaults = {"port": 8080, "host": "0.0.0.0", "workers": 4} %}
{% set overrides = {"port": 9090, "workers": 16} %}

app: {{ defaults | dict_merge(overrides) | to_json }}
# Output: app: {"host":"0.0.0.0","port":9090,"workers":16}
Merging settings with per-environment overrides
{% set base_config = settings.app.defaults %}
{% set env_config = settings.app.environments[facts.environment] %}

app_config: {{ base_config | dict_merge(env_config) | to_json }}

Shallow merge

dict_merge performs a shallow merge. Nested dictionaries are replaced entirely, not merged recursively. If you need deep merge behavior, structure your settings files to use Zester's built-in deep merge during settings compilation.


Built-in Gonja/Jinja2 Filters

All standard Gonja filters are available. These are the most commonly used in Zester templates:

String Filters

FilterDescriptionExample
upperConvert to uppercase{{ "hello" | upper }} -- HELLO
lowerConvert to lowercase{{ "HELLO" | lower }} -- hello
titleCapitalize each word{{ "hello world" | title }} -- Hello World
capitalizeCapitalize first character{{ "hello" | capitalize }} -- Hello
trimStrip leading/trailing whitespace{{ " hello " | trim }} -- hello
truncate(n)Truncate to n characters{{ "hello world" | truncate(5) }} -- he...
replace(old, new)Replace substring{{ "foo-bar" | replace("-", "_") }} -- foo_bar
wordcountCount words{{ "hello world" | wordcount }} -- 2

List Filters

FilterDescriptionExample
join(sep)Join list with separator{{ [1, 2, 3] | join(", ") }} -- 1, 2, 3
firstFirst item in list{{ [1, 2, 3] | first }} -- 1
lastLast item in list{{ [1, 2, 3] | last }} -- 3
lengthNumber of items{{ [1, 2, 3] | length }} -- 3
sortSort a list{{ [3, 1, 2] | sort }} -- [1, 2, 3]
reverseReverse a list{{ [1, 2, 3] | reverse }} -- [3, 2, 1]
uniqueRemove duplicates{{ [1, 1, 2] | unique }} -- [1, 2]
reject(test)Remove items matching test{{ [1, 2, 3] | reject("odd") }}
select(test)Keep items matching test{{ [1, 2, 3] | select("odd") }}
map(attr)Extract attribute from items{{ users | map(attribute="name") }}
batch(n)Group into batches of n{{ [1,2,3,4] | batch(2) }}

Value Filters

FilterDescriptionExample
default(val)Fallback for undefined/empty{{ x | default("none") }}
intConvert to integer{{ "42" | int }} -- 42
floatConvert to float{{ "3.14" | float }} -- 3.14
stringConvert to string{{ 42 | string }} -- "42"
absAbsolute value{{ -5 | abs }} -- 5
roundRound a float{{ 3.7 | round }} -- 4.0

Practical Examples

Building a comma-separated allow list
allowed_ips: {{ facts.network.ipv4 | join(",") }}
Safe hostname normalization
normalized_host: {{ facts.network.hostname | lower | replace(".", "-") | truncate(63) }}
Conditional defaults with type conversion
worker_count: {{ settings.workers | default(facts.cpu.count) | int }}
Sorting and filtering mount points
data_mounts:
  {% for mount in facts.disk.mounts | sort(attribute="mountpoint") %}
  {% if mount.mountpoint | regex_match("/data") %}
  - {{ mount.mountpoint }}
  {% endif %}
  {% endfor %}

On this page