Reference

Tera Function & Filter Reference

Complete reference for Tera functions and filters added by rescile, including project Rhai libraries, CIDR helpers, JMESPath, HMAC signing, date utilities, and encoding.

Tera Function & Filter Reference

Custom and contributed functions and filters available in rescile’s Tera v2 templating engine.

rescile uses Tera v2 with custom and contributed logic for infrastructure modeling. These extensions are available on project-aware template surfaces, including models, compliance rules, outputs, generators, external fetchers, actions, contracts, asset templates, and proxy rules. For an overview of how templating fits into the overall data context, see Templating & Data.


Delimiters

rescile supports all standard Tera delimiters:

Delimiter Syntax Usage
Expressions {{ ... }} Interpolation and filters (e.g. {{ origin_resource.name | upper }}).
Blocks {% ... %} Control flow, conditionals, and loops (e.g. {% if origin_resource.env == 'prod' %}...{% endif %}).
Comments {# ... #} Template comments stripped during rendering (e.g. {# internal note #}).

Whitespace trimming is supported on all delimiters using hyphens (e.g. {{- value -}}, {%- if cond -%}, {#- comment -#}).


Project library calls with lib

Use lib to call a pure function from a .rhai script. This keeps project-specific calculations (such as network math, naming rules, or complex lookups) outside rescile-controller while making them directly available to Tera templates.

Put project-owned libraries below <data-dir>/lib. A reusable module can provide libraries in its own lib/ directory:

data/
├── lib/
│   ├── network.rhai          # Rhai script callable from Tera
│   └── helpers.libsonnet     # Libsonnet helper imported by Jsonnet models/compliance
└── models/
    └── network-plan.toml

Note on lib/ directory usage: The lib/ directory houses two distinct library types:

  • .rhai files: Procedural functions invoked dynamically by Tera templates via lib(...).
  • .libsonnet files: Declarative libraries imported by Jsonnet models, compliance rules, and output templates (import '...').

Define each exported Rhai function with two parameters: value and args.

// data/lib/network.rhai
fn label(value, args) {
    return args.prefix + value;
}

fn plan(value, args) {
    return #{
        cidr: args.cidr,
        hosts: args.hosts,
        standalone: value == ()
    };
}

Filter form

The filter form passes the piped value as value:

[create_resource.properties]
label = "{{ origin_resource.cidr | lib(path='network.rhai', function='label', prefix='cidr:') }}"

The example calls:

label(origin_resource.cidr, #{ prefix: "cidr:" })

Function form

The function form passes null (Rhai unit ()) as value:

[create_resource.properties]
plan = "{{ lib(path='network.rhai', function='plan', cidr=origin_resource.cidr, hosts=20) }}"

The example calls:

plan((), #{ cidr: origin_resource.cidr, hosts: 20 })

Arguments and values

Parameter Required Description
path Yes Relative .rhai path below a configured lib/ directory. Do not include the lib/ prefix.
function Yes Name of the exported function.
Other named arguments No Values collected into the args map.

Inputs and results can be null, booleans, integers, floats, strings, arrays, or object maps. The library receives only the piped value and explicitly named arguments. It cannot read the remaining Tera context unless you pass values from that context.

Resolution and safety

rescile resolves a requested path in this order:

  1. <data-dir>/lib;
  2. each resolved module’s lib/ directory, in module order (dependency order for git modules; CLI argument order for local --module <path> modules).

The first match wins. This lets a project replace a module library with a local file at the same path. rescile reports a warning through the importer log when later matches are shadowed (Project library '...' resolves to '...'; shadowed matches: ...), once per path per build.

Paths must be relative, include the .rhai extension, and cannot contain ... Symlinks and nested imports cannot escape configured project or module library roots.

Rhai import statements inside a script resolve against <data-dir>/lib only — never relative to the importing script. A script resolved from a module’s lib/ cannot import its own sibling modules; place shared Rhai modules in the workspace lib/ instead:

// data/lib/network.rhai — resolves because <data-dir>/lib is the module base
import "shared/prefix" as p;

fn label(value, args) {
    return p::prefix() + value;
}

For comparison, .libsonnet imports use a sibling-first chain — see Auto-Discovery from lib/ in the Jsonnet models guide.

Library errors fail the current build, request, mutation, or action expansion. Rhai scripts run with strict engine limits (max_operations, call depth, expr depth, string and collection sizes) to prevent runaway execution or memory exhaustion.

Sandboxed file access: read_input_json

Rhai scripts cannot access arbitrary host files. However, rescile exposes the sandboxed helper read_input_json(filename) to load and query reference data located in input/ directories:

// data/lib/lookup.rhai
fn lookup_code(code, _args) {
    let table = read_input_json("codes.json");
    let entry = table[code];
    if entry != () {
        return entry.description;
    }
    return "";
}
  • Confinement: The requested filename must be relative and resolve strictly inside a project or module input/ directory. Absolute paths (/etc/...), parent traversal (..), and symlinks escaping input/ are strictly blocked.
  • Lookup order: <data-dir>/input/ first, then each module’s input/ in module order. Shadowed matches in later roots are reported through the importer log (Input file '...' resolves to '...'; shadowed matches: ...), once per file per build.
  • In-Memory Caching: Data files read through read_input_json are parsed into Rhai data structures and cached in memory for the importer run. Repeated calls across thousands of graph vertices incur zero repeated disk I/O.

Sandboxed rhai builtin inventory

Sandboxed rhai scripts run with the core language only (no std::fs, no network, no import of host code) plus the following rescile-registered extensions. Knowing the exact inventory avoids re-implementing string surgery in every project:

String utilities (from rhai’s MoreStringPackage): trim, replace, to_upper, to_lower, sub_string, split, contains, index_of, etc. Note that trim and replace mutate the string variable in place and return () — call them on a let binding, not on a literal:

fn clean(value, _args) {
    let s = value;
    s.trim();
    s.replace(" ", "_");
    return s;
}

Native helpers registered by rescile (deterministic, read-only — the same primitives as the equivalent Tera filters):

Function Signature Description
sha256 sha256(data) -> String SHA-256 hash as lowercase hex.
hmac_sha256 hmac_sha256(data, key) -> String HMAC-SHA256 as lowercase hex.
b64url_encode b64url_encode(data) -> String Base64url without padding (RFC 7515).
b64url_decode b64url_decode(text) -> String Inverse of b64url_encode; errors on invalid input.
rs256_verify rs256_verify(signing_input, jwk, sig_b64url) -> bool RSA PKCS#1 v1.5 (RS256) signature check against a JSON JWK.
es256_verify es256_verify(signing_input, jwk, sig_b64url) -> bool ECDSA P-256 (ES256) signature check against a JSON JWK.
jws_verify jws_verify(jws, jwk) -> bool Verify a compact header.payload.signature JWS; accepts alg RS256/ES256.
is_null is_null(x) -> bool Detect the unit value produced by bridged JSON null (the x == () check is unreliable; type_of(x) works too).

This makes RFC 8555 JWS verification possible directly in policy rules — e.g. verify an ACME server’s JWS over the account URL and nonce before trusting a challenge response — without a Python runtime. jws_verify only checks signature authenticity; protocol checks (nonce replay, url header equality, key authorization) remain the responsibility of the caller.

Rhai language rules to keep in mind:

  • Module-level const values are not visible inside functions in the same file (functions never capture the calling scope). Pass constants as parameters or wrap them in a function.
  • protected is a reserved keyword in current rhai versions; do not use it as an identifier or a map key accessed via dot notation.
  • JSON values bridged from Tera arrive as rhai strings/numbers/arrays/maps; JSON null becomes the unit value ().

Functions

counter(key=value)

Returns a stateful, zero-based incrementing integer for a given unique key. Each unique key has its own independent counter, reset on every importer run. This is more powerful than the built-in origin_resource_counter variable when you need independent counters per group.

Parameter Description
key A value uniquely identifying the counter. Can be a string, number, boolean, an object with a name property, or an array of such types (which will be sorted and joined).

Example — allocate /24 subnets per region, restarting the index for each:

origin_resource = "department"

[[create_resource]]
resource_type = "subnet"
relation_type = "HAS_SUBNET"
[create_resource.properties]
# For departments in '10.0.0.0/16', counter goes 0, 1, 2...
# For departments in '10.1.0.0/16', counter independently goes 0, 1, 2...
subnet_cidr = "{{ origin_resource.region[0].cidr | cidr_nth_subnet(prefix=24, nth=counter(key=origin_resource.region)) }}"

calculate_cidr(ips=[...])

Calculates the smallest possible CIDR block that contains a given list of IP addresses.

Parameter Description
ips An array of strings, each a valid IPv4 or IPv6 address.

Example — summarize server IPs into a containing CIDR:

origin_resource = "application"

[[create_resource]]
resource_type = "network_summary"
name = "summary_for_{{ origin_resource.name }}"
[create_resource.properties]
# map(attribute='ip_address') extracts each server's IP, then calculate_cidr wraps them.
# For ips=["10.1.5.10", "10.1.5.25"] → "10.1.5.0/27"
summary_cidr = "{{ calculate_cidr(ips=origin_resource.server | map(attribute='ip_address')) }}"

allocate_subnets(cidr="string", host_map={...})

Dynamically plans a subnet layout. Takes a parent CIDR block and a map of subnet names to required host counts. Calculates the smallest possible subnet for each entry and allocates them sequentially. Keys are sorted deterministically before allocation.

Parameter Description
cidr The parent CIDR block from which to allocate.
host_map A dictionary mapping subnet names to required host counts.

Example — auto-plan a three-tier network layout:

origin_resource = "network"

required_subnets = { dmz = 10, app = 100, db = 50 }

[[create_resource]]
match_on = [{ property = "name", value = "core_network" }]
resource_type = "network_layout"
name = "layout_for_{{ origin_resource.name }}"
[create_resource.properties]
# Keys are sorted (app, db, dmz) before allocation for determinism.
# For cidr="10.0.0.0/16":
# → { "app": "10.0.0.0/25", "db": "10.0.0.128/26", "dmz": "10.0.0.192/28" }
allocated_cidrs = "{{ allocate_subnets(cidr=origin_resource.cidr, host_map=required_subnets) }}"

The result is a JSON object that can be used in subsequent models to create the actual subnet resources.


random_string(length=int)

Generates a random alphanumeric string of the specified length. Useful for generating temporary passwords, ACME challenge tokens, or unique identifiers.

Parameter Description
length (Optional) The number of characters to generate. Defaults to 32.

Example — generate an ACME challenge token:

[create_resource.properties]
challenge_token = "{{ random_string(length=48) }}"

Filters

cidr_nth_subnet(prefix=int, nth=int)

Extracts a specific zero-indexed subnet from a parent CIDR block. The prefix parameter is the absolute prefix length of the desired subnet, not the number of bits to add.

Parameter Description
prefix Absolute target prefix length (e.g., 24 for /24 subnets). Must be larger than or equal to the parent CIDR’s prefix.
nth Zero-based index of the subnet to select.

Example — allocate ordered /24 subnets from a /16 block:

origin_resource = "department"

[[create_resource]]
resource_type = "subnet"
relation_type = "HAS_SUBNET"
[create_resource.properties]
# origin_resource_counter is 0 for the first department, 1 for the second, etc.
# Result: "10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24", ...
subnet_cidr = "{{ '10.0.0.0/16' | cidr_nth_subnet(prefix=24, nth=origin_resource_counter) }}"

cidr_split_n(n=int)

Splits a CIDR block into n equal-sized subnets. The count is rounded up to the next power of two. Returns an array of CIDR strings.

Parameter Description
n Desired number of subnets. Rounded up to next power of two.

Example — divide a /16 into 4 equal /18 subnets:

origin_resource = "vpc"

[[create_resource]]
resource_type = "network_layout"
name = "layout_for_{{ origin_resource.name }}"
[create_resource.properties]
# For origin_resource.cidr = "10.10.0.0/16":
# → ["10.10.0.0/18", "10.10.64.0/18", "10.10.128.0/18", "10.10.192.0/18"]
regional_subnets = "{{ origin_resource.cidr | cidr_split_n(n=4) }}"

jmespath(query="string")

Executes a JMESPath query against a JSON-compatible value (object, array, or string). Returns the query result. If no match is found, returns an empty value (falsy).

This is useful both for extracting nested data in templates and for filtering in match_on expressions. See Matching on JSON with jmespath for the match_on usage pattern.

Parameter Description
query A valid JMESPath expression string.

Example — extract a nested property:

[create_resource.properties]
# origin_resource.config = { "storage": { "class": "premium-ssd" } }
storage_class = "{{ origin_resource.config | jmespath(query='storage.class') }}"
# → "premium-ssd"

Example — pre-filter JSON data in the file header:

# Load SLAs and select only the first critical-level policy before the loop starts.
critical_sla = { "json!" = "slas.json", jmespath = "policies[?level == 'critical'] | [0]" }

[[create_resource]]
[create_resource.properties]
backup_frequency = "{{ critical_sla.backup_frequency }}"

A templated json! path is evaluated for each model origin before its JMESPath expression runs:

provider = { "function!" = "{{ origin_resource.provider | lower }}" }
compute_role = { "json!" = "{{ provider }}-roles.json", jmespath = "values(roles)[?service == 'compute'] | [0]" }

Each distinct input file and filtered result is cached for the current import run. Dynamic paths are limited to model headers. They can drive create_from.list: globally resolvable paths execute once, while paths that transitively depend on origin_resource execute once per origin and expose both origin_resource and value in the inner loop. A static header list (literal array or globally resolved variable) combined with a declared origin_resource likewise executes once per origin and then per item. A path cannot depend on value because the list must be loaded before value exists. See Templating and Data Manipulation for execution order, path restrictions, and errors.

Example — match_on expression with jmespath:

[[create_resource]]
match_on = [
  {
    expression = """
      {%- set tags = origin_resource.config
                     | jmespath(query="tags[?key == 'env' && value == 'prod']") -%}
      {% if tags %}true{% endif %}
    """
  }
]
resource_type = "backup_policy"
name = "backup_policy_for_{{ origin_resource.name }}"

map_format(format="string", attribute="string")

Applies a format string to each element of an array, or to a specific attribute of each object within an array. Replaces {} in the format string with the extracted value.

Parameter Description
format The format string containing {} where the value should be inserted.
attribute (Optional) The name of the property to extract from matching objects before formatting. Supports dot-notation.

Example — generate a list of secret names:

[create_resource.properties]
# origin_resource.backup = [{name: "db1"}, {name: "db2"}]
# → ["secret-restic-password-db1", "secret-restic-password-db2"]
secret_names = "{{ origin_resource.backup | map_format(attribute='name', format='secret-restic-password-{}') }}"

select(from="string", match="key=value")

Filters an array of objects by a condition and extracts a specific property from each matching object. Can also operate on a single object, returning the property value if the condition matches or null otherwise.

Parameter Description
from Property name to extract from matching objects. Supports dot-notation (e.g., "config.name").
match Condition in "key.path=value" format. Dot-notation supported. If the target property is an array, the filter checks whether it contains the value.

Example — extract names of active servers:

[create_resource.properties]
# origin_resource.server = [{name: "srv1", status: "active"}, {name: "srv2", status: "inactive"}]
# select extracts 'name' where status=active → ["srv1"]
# join converts the array → "srv1"
active_servers = "{{ origin_resource.server | select(from='name', match='status=active') | join(sep=',') }}"

Example — single object:

# origin_resource.primary_db = { name: "db1", role: "primary" }
primary_db_name = "{{ origin_resource.primary_db | select(from='name', match='role=primary') }}"
# → "db1"

intersect(with=[...])

Returns a new array containing only elements present in both the input array and the with array. Duplicates are removed from the result.

Parameter Description
with The second array to intersect with.

Example — find common tags:

[create_resource.properties]
# application.tags = ["frontend", "web", "prod"]
# server.tags      = ["web", "api", "prod"]
# → ["web", "prod"]
common_tags = "{{ origin_resource.tags | intersect(with=origin_resource.server[0].tags) }}"

slice(start=0, end=length)

Extracts a subslice from an array or string. start is inclusive (defaults to 0) and end is exclusive (defaults to the total length). Supports negative indices counting from the end of the collection.

Provided as a backward-compatibility filter for templates authored in Tera v1, and for chaining inside filter pipelines (such as split | slice | join).

Parameter Description
start (Optional) 0-indexed start position (inclusive). Default: 0. Negative numbers count backwards from the end.
end (Optional) 0-indexed end position (exclusive). Default: collection length. Negative numbers count backwards from the end.

Example — slice an array in a pipeline:

[create_resource.properties]
# origin_resource.name = "AWS Transit Hub"
# split(" ") → ["AWS", "Transit", "Hub"]
# slice(start=1) → ["Transit", "Hub"]
# join("") | lower → "transithub"
hub_suffix = "{{ origin_resource.name | split(pat=' ') | slice(start=1) | join(sep='') | lower }}"

Example — subslice tags with start and end:

[create_resource.properties]
# tags = ["prod", "frontend", "web", "internal"]
# slice(start=1, end=3) → ["frontend", "web"]
core_tags = "{{ origin_resource.tags | slice(start=1, end=3) }}"
# slice(start=0, end=-1) → ["prod", "frontend", "web"]
leading_tags = "{{ origin_resource.tags | slice(start=0, end=-1) }}"

Note on Tera v2 syntax: In Tera v2, variables can also be sliced directly using bracket syntax, e.g. {{ origin_resource.tags[1:3] }} or {{ origin_resource.name[:3] }}. The | slice(...) filter remains supported for backward compatibility and chaining after other filters.


concat(with=[...])

Concatenates an array or appends a single value to the target array. Returns a new combined array.

Provided as a backward-compatibility filter for legacy Tera v1 templates.

Parameter Description
with An array to merge into the input array, or a single value to append.

Example — merge lists:

[create_resource.properties]
# tags = ["web", "api"]
# concat(with=["prod", "dmz"]) → ["web", "api", "prod", "dmz"]
all_tags = "{{ origin_resource.tags | concat(with=['prod', 'dmz']) }}"
# concat(with="primary") → ["web", "api", "primary"]
tagged = "{{ origin_resource.tags | concat(with='primary') }}"

Tip: In new templates, prefer Tera v2 list syntax ([...arr1, ...arr2]) over | concat. Array concatenation with the + operator is not supported — + requires numeric operands.


filter(attribute="...", value=...)

Keeps the array items whose attribute matches value. Without value (or with value=null), keeps the items where attribute is present and non-null. attribute supports dot-notation paths. Undefined input yields an empty array.

Provided as a backward-compatibility filter for legacy Tera v1 templates.

Parameter Description
attribute Dot-notation path evaluated on each item (required).
value (Optional) Comparison value. Omitted/null: keep items where the attribute is present and non-null.

Example — filter relation neighbors:

[create_resource.properties]
# Keep only LAN segments from the HAS_SEGMENT relation
lan_segments = "{{ origin_resource.HAS_SEGMENT | default(value=[]) | filter(attribute=\"address_source\", value=\"lan\") }}"

Tip: In new templates, prefer the Tera v2 list comprehension [s for s in origin_resource.HAS_SEGMENT if s.address_source == 'lan'] over | filter.


regexp(expr="s/pattern/replacement/flags")

Applies a sed-style regular-expression substitution to a string. The delimiter is the character immediately following the leading s and can be any character (commonly / or |).

Parameter Description
expr (alias: pattern) A sed substitution expression, e.g. s/foo/bar/gi.

Flags:

Flag Meaning
g Global — replace all occurrences, not just the first.
i / I Case-insensitive matching.

Replacement syntax:

Token Expands to
\1–\9 Captured group N.
& Entire match.
\n \t \r Newline, tab, carriage return.
\\ Literal backslash.
\<delim> Literal delimiter character.

Example — strip a domain suffix:

[create_resource.properties]
# "WebServer01.corp.example.com" → "webserver01"
short_name = "{{ origin_resource.fqdn | regexp(expr='s/\\.corp\\.example\\.com$//i') | lower }}"

Example — backreferences:

[create_resource.properties]
# "10.0.0.0/16" → "net_10.0.0.0_prefix16"
label = "{{ origin_resource.cidr | regexp(expr='s/([0-9.]+)\\/([0-9]+)/net_\\1_prefix\\2/') }}"

Example — custom delimiter to avoid escaping slashes:

[create_resource.properties]
# "a/b/c" → "a-b-c"
safe_path = "{{ origin_resource.path | regexp(expr='s|/|-|g') }}"

Tip: When your pattern or replacement contains the / character, use an alternative delimiter (like |, #, or @) to keep the expression readable: s|old/path|new/path|g instead of s/old\/path/new\/path/g.


regex_replace(pattern="pattern", rep="replacement")

Replaces all occurrences matching pattern with rep. Provided by upstream tera-contrib.

Parameter Type Required Description
pattern string Yes Regular expression to match. Supports inline flags such as (?i) for case-insensitivity.
rep string Yes Replacement text. Supports Rust regex capture syntax ($1, ${1}, $name).

regexp vs regex_replace:

  • Use regex_replace(pattern=..., rep=...) when you prefer separate pattern and replacement arguments, standard Rust regex replacement syntax ($1), or global replacement.
  • Use regexp(expr="s/pat/rep/flags") when you need sed syntax, custom delimiters (s#...#...#), backreferences (\1, &), or single-match replacement (omit g). Both filters cache compiled expressions.

Example — strip a domain suffix:

[create_resource.properties]
short_name = "{{ origin_resource.fqdn | regex_replace(pattern='(?i)\\.corp\\.example\\.com$', rep='') | lower }}"

Example — reorder capture groups:

[create_resource.properties]
name = "{{ origin_resource.full_name | regex_replace(pattern='(?P<last>[^,]+),\\s+(?P<first>\\S+)', rep='$first $last') }}"

matching(pat="pattern") (Test)

Tests whether a string matches a regular expression. Provided by upstream tera-contrib.

{% if origin_resource.hostname is matching(pat="^web-[0-9]+") %}
...
{% endif %}

sha256

Computes the SHA-256 hash of a value. Strings are hashed directly; complex types (arrays, objects) are first serialized to JSON. Returns a lowercase hexadecimal string.

Example — create a unique, deterministic ID:

[create_resource.properties]
# Combine address and port into a stable hash to avoid name collisions.
unique_id = "{{ [origin_resource.address, origin_resource.port] | json_encode | sha256 }}"

base64_encode

Encodes a value using standard Base64. Strings are encoded directly; complex types are serialized to JSON first.

Example — encode a startup script for cloud-init:

[create_resource.properties]
user_data = "{{ origin_resource.startup_script | base64_encode }}"

hmac_sha256(key="secret", encoding="hex"|"base64")

Computes HMAC-SHA256 of a value using the given key. Returns a hex string by default; pass encoding="base64" for Base64 output.

Parameter Description
key The secret key for the HMAC computation. Supports {{ env.VAR }} expansion.
encoding Output encoding: "hex" (default) or "base64".

This filter is primarily used in proxy.toml for request signing, but is available everywhere.

Example — sign a payload for API authentication:

[create_resource.properties]
signature = "{{ origin_resource.payload | hmac_sha256(key=env.API_SECRET, encoding=\"base64\") }}"

Example — Exoscale API request signing in proxy.toml:

# app/proxy.toml
[[route]]
path = "/api/exoscale"
target = "https://api-ch-dk-2.exoscale.com/v2"
forward_headers = ["Accept", "User-Agent"]
[route.inject_headers]
"Content-Type" = "application/json"
"Authorization" = """
{%- set expires = request.timestamp + 600 -%}
{%- set msg = request.method ~ " " ~ request.path ~ "\n" ~ request.body ~ "\n" ~ request.query_values ~ "\n\n" ~ expires -%}
{%- set sig = msg | hmac_sha256(key=env.EXOSCALE_API_SECRET, encoding="base64") -%}
EXO2-HMAC-SHA256 credential={{ env.EXOSCALE_API_KEY }},expires={{ expires }},signature={{ sig -}}
"""

For the full proxy configuration reference, see proxy.toml Reference.


outbound / inbound

Select the neighbors of a relation list by edge direction. origin_resource.<RELATION> collects neighbors from both directions of the relation; each neighbor carries _relation.direction ("outbound" or "inbound"), which these filters expose:

[create_resource.properties]
issuer_count = "{{ origin_resource.SIGNED_BY | outbound | length }}"
issuers     = "{{ origin_resource.SIGNED_BY | outbound | json_encode | safe }}"

Neighbors without a _relation marker (e.g. hand-constructed arrays) are excluded. Passing a non-array value is a template error.


Standard Tera Features Used in rescile

In addition to the custom extensions above, rescile configurations make heavy use of standard Tera features. The full list is in the Tera documentation.

Commonly Used Built-in Filters

Filter Example Description
upper / lower {{ name | upper }} Change string case.
trim / trim_start / trim_end {{ s | trim_start(pat="v") }} Strip whitespace or matching pattern prefix/suffix (pat).
trim_start_matches / trim_end_matches {{ s | trim_start_matches(pat="v") }} Strip prefix/suffix pattern (Tera v1 compatibility).
replace(from, to) {{ name | replace(from=".", to="-") }} Replace substrings.
default(value) {{ x | default(value="n/a") }} Fallback when value is undefined.
split(pat) {{ v | split(pat=".") | first }} Split a string and pick an element.
slice(start, end) {{ arr | slice(start=1) }} Slice array or string (Tera v1 compatibility; or arr[1:]).
concat(with) {{ arr | concat(with=extra) }} Append or merge into array (Tera v1 compatibility).
filter(attribute, value) {{ arr | filter(attribute=\"source\", value=\"lan\") }} Keep array items by attribute (Tera v1 compatibility).
join(sep) {{ arr | join(sep=",") }} Join array elements into a string.
map(attribute) {{ servers | map(attribute="ip") }} Extract one property from each object in an array.
json_encode {{ obj | json_encode | safe }} Serialize to JSON string (use safe to avoid escaping).
length {{ items | length }} Count items in an array or string.
first / last / nth {{ arr | first }} Pick first, last, or Nth array element. Returns null if empty.
unique {{ arr | unique }} Remove duplicate elements from array.
date(format) {{- now(utc=true) | date(format="%Y-%m-%dT%H:%M:%SZ") -}} Format a date/datetime using strftime formatting (powered by jiff).
b64_encode / b64_decode {{ val | b64_encode }} Base64 encode and decode strings.
slug {{ name | slug }} Convert text into a URL/identifier-friendly slug.
urlencode {{ query | urlencode }} Percent-encode URI parameters.

Conditionals and Loops

[create_resource.properties]
# Inline conditional
sla = "{% if origin_resource.environment == 'prod' %}Gold{% else %}Silver{% endif %}"

# Loop to build a comma-separated list
tag_list = "{% for t in origin_resource.tags %}{{ t | upper }}{% if not loop.last %},{% endif %}{% endfor %}"

# Default filter to avoid undefined errors
region = "{{ origin_resource.region | default(value='us-east-1') }}"

List Comprehensions (Tera v2)

Tera v2 introduces native Python-style list comprehensions. This replaces the verbose Tera v1 pattern of initializing an empty list and using set_global in a loop:

{# Tera v2: list comprehension #}
{% set active_ips = [server.ip for server in origin_resource.servers if server.active] | sort %}

{# Tera v1 legacy pattern (deprecated): #}
{% set active_ips = [] %}
{% for server in origin_resource.servers %}
  {% if server.active %}
    {% set_global active_ips = active_ips | concat(with=server.ip) %}
  {% endif %}
{% endfor %}
{% set active_ips = active_ips | sort %}

Comprehensions are concise, avoid global variable mutation, and can be chained directly into filters such as | sort or | unique.

Tests and Conditionals (Tera v2 Keyword Arguments)

In Tera v2, tests that take arguments require keyword arguments (e.g. pat=, divisor=, other=):

{# Tera v2 keyword syntax #}
{% if clean_filename is ending_with(pat=".sh") %}...{% endif %}
{% if fqdn is starting_with(pat="web-") %}...{% endif %}
{% if tag_list is containing(pat="prod") %}...{% endif %}
{% if step is divisible_by(divisor=2) %}...{% endif %}
{% if date is before(other="2026-01-01") %}...{% endif %}
{% if config is map %}...{% endif %}
Test Tera v2 Syntax Description
ending_with is ending_with(pat="...") True if string ends with pattern.
starting_with is starting_with(pat="...") True if string starts with pattern.
containing is containing(pat="...") True if string, array, or map contains pattern/key.
matching is matching(pat="^...") Regex match test (from tera_contrib).
divisible_by is divisible_by(divisor=n) True if integer divisible by divisor.
map is map True if value is an object/map (renamed from object in Tera v1).
before / after is before(other="...") Date comparison test (from tera_contrib).

Backward Compatibility: rescile automatically normalizes legacy Tera v1 positional calls (e.g. is ending_with(".sh"), is divisibleby(2), and is object) to their Tera v2 keyword equivalents during template preprocessing. Both legacy positional and v2 keyword syntax are accepted.

Traversing relationships in templates uses dot notation. Multi-valued relations are arrays, and traversal is undirected — both edge directions appear in the array, ordered by neighbor name. Use the outbound / inbound filters (or _relation.direction) when a relation label exists in both directions:

[create_resource.properties]
# First related database's version
db_version      = "{{ origin_resource.database[0].version }}"
# Property on the connecting edge
connection_type = "{{ origin_resource.database[0]._relation.label }}"
# Direction of the edge that produced the neighbor
edge_direction  = "{{ origin_resource.database[0]._relation.direction }}"
# Collect all volumes from related applications
volumes = "{{ origin_resource.application | map(attribute='volume') | json_encode | safe }}"

Quick Reference

Name Type Signature Description
lib Function lib(path="x.rhai", function="f", ...) Call a project Rhai function with value = () (null).
lib Filter | lib(path="x.rhai", function="f", ...) Call a project Rhai function with the piped value.
counter Function counter(key=value) Stateful per-key incrementing integer.
calculate_cidr Function calculate_cidr(ips=[...]) Smallest CIDR containing given IPs.
allocate_subnets Function allocate_subnets(cidr="...", host_map={...}) Dynamic subnet planning from host counts.
cidr_nth_subnet Filter | cidr_nth_subnet(prefix=int, nth=int) Extract the Nth subnet of a given prefix size.
cidr_split_n Filter | cidr_split_n(n=int) Split CIDR into N equal subnets.
jmespath Filter | jmespath(query="string") JMESPath query against a JSON structure.
map_format Filter | map_format(format="...", attribute="...") Format elements of an array.
select Filter | select(from="prop", match="k=v") Filter array and extract a property.
regexp Filter | regexp(expr="s/pat/repl/flags") Sed-style regex substitution with backreferences and sed flags.
outbound Filter | outbound Neighbors of a relation list with outgoing edge direction.
inbound Filter | inbound Neighbors of a relation list with incoming edge direction.
regex_replace Filter | regex_replace(pattern="...", rep="...") Standard regex replace all (upstream tera-contrib).
matching Test is matching(pat="...") Regex match test (upstream tera-contrib).
intersect Filter | intersect(with=[...]) Common elements of two arrays.
slice Filter | slice(start=int, end=int) Subslice of an array or string.
concat Filter | concat(with=val|[...]) Append or concatenate into an array.
filter Filter | filter(attribute=\"...\", value=...) Keep array items whose attribute matches (Tera v1 compatibility).
trim_start_matches Filter | trim_start_matches(pat="...") Strip prefix pattern (Tera v1 compatibility).
trim_end_matches Filter | trim_end_matches(pat="...") Strip suffix pattern (Tera v1 compatibility).
sha256 Filter | sha256 SHA-256 hex hash.
base64_encode Filter | base64_encode (or | b64_encode) Base64 encoding.
base64_decode Filter | base64_decode (or | b64_decode) Base64 decoding.
hmac_sha256 Filter | hmac_sha256(key="...", encoding="hex"|"base64") HMAC-SHA256 signature.
date Filter | date(format="...") Date/time formatter.
now Function now(utc=true) Current timestamp.