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: Thelib/directory houses two distinct library types:
.rhaifiles: Procedural functions invoked dynamically by Tera templates vialib(...)..libsonnetfiles: 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:
<data-dir>/lib;- 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 escapinginput/are strictly blocked. - Lookup order:
<data-dir>/input/first, then each module’sinput/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_jsonare 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
constvalues 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. protectedis 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
nullbecomes 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|ginstead ofs/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). |
regexpvsregex_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 (omitg). 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), andis object) to their Tera v2 keyword equivalents during template preprocessing. Both legacy positional and v2 keyword syntax are accepted.
Accessing Related Resources
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. |