Templating & Data
Dynamic Values, External Data, and Multiple Templating Engines
rescile supports several templating and data-querying technologies that you can use inside your TOML
configuration files. This page explains each capability and how to choose between them.
| Technology | Where used | Purpose |
|---|---|---|
| Tera | models/*.toml, compliance/*.toml, output/*.toml, proxy.toml |
String interpolation, conditionals, loops, filters |
| Jsonnet | models/*.jsonnet, compliance/*.jsonnet, output/*.jsonnet, output/ (jsonnet key) |
First-class functional rule authoring, comprehensions, matrix expansions, shared libraries (.libsonnet), and structured JSON generation |
| JMESPath | Header json! pre-filter, jmespath Tera filter |
Querying and reshaping JSON data |
1. Tera Templating
All string values in name, properties, match_on, filename, and template fields are processed
by the Tera templating engine. Tera syntax uses {{ expression }}
for values and {% tag %} for control flow.
For the complete list of rescile-specific functions and filters, see Tera Function & Filter Reference.
Reuse project calculations with lib
Put pure project calculations in <data-dir>/lib/*.rhai, then call exported functions from Tera
with the lib filter or function. Use the filter when the primary input is already in the pipeline:
// data/lib/naming.rhai
fn qualify(value, args) {
return args.environment + "-" + value;
}
fn descriptor(value, args) {
return #{
name: args.name,
tier: args.tier
};
}
[create_resource.properties]
qualified_name = "{{ origin_resource.name | lib(path='naming.rhai', function='qualify', environment=params.environment) }}"
descriptor = "{{ lib(path='naming.rhai', function='descriptor', name=origin_resource.name, tier=origin_resource.tier) }}"
Every exported function accepts (value, args). Filter calls pass the piped value; function calls pass
null (received as Rhai ()). Only named arguments other than path and function enter args. Note that
the lib/ directory also stores .libsonnet files for imports in Jsonnet models, compliance, and outputs.
See Project library calls with lib for
layout, resolution order, supported values, and path restrictions.
The origin_resource Variable
The most common data source in Tera templates. Inside any Rule Block, origin_resource refers to the
specific resource being processed in the current loop iteration.
origin_resource = "application"
[[create_resource]]
resource_type = "server"
name = "server_for_{{ origin_resource.name }}"
[create_resource.properties]
hostname = "{{ origin_resource.name | upper }}"
tier = "{% if origin_resource.environment == 'prod' %}premium{% else %}standard{% endif %}"
When processing the billing-api application, {{ origin_resource.name }} renders as billing-api.
The value Variable (from create_from)
When create_from.list uses a globally resolved header list, origin_resource is not available and
the current list item is exposed as {{ value }}. When a dynamic JSON list transitively depends on
origin_resource, rescile runs a nested loop and exposes both origin_resource and value.
provider_list = ["aws", "azure", "gcp"]
[[create_resource]]
create_from = { list = "provider_list", as = "provider" }
name = "cloud-{{ value }}"
[create_resource.properties]
short_name = "{{ value | upper }}"
During the first iteration {{ value }} is aws, second is azure, and so on.
Inline TOML Data
Any top-level key in the file header that is not a reserved rescile directive becomes a template variable available to all Rule Blocks in that file.
origin_resource = "application"
server_specs = { standard_cpu = 4, premium_cpu = 8 }
[[create_resource]]
resource_type = "server"
name = "server_{{ origin_resource.name }}"
[create_resource.properties]
cpu_cores = "{% if origin_resource.environment == 'prod' %}{{ server_specs.premium_cpu }}{% else %}{{ server_specs.standard_cpu }}{% endif %}"
External JSON Files (json!)
Load local lookup data with the json! directive. Paths are relative to the input directory, such as
data/input/ for a project data directory.
A literal path is loaded once while rescile parses the model:
origin_resource = "server"
ip_ranges = { "json!" = "shared/ip_ranges.json" }
[[create_resource]]
[create_resource.properties]
subnet = "{{ ip_ranges.subnets[origin_resource.region] }}"
Select a JSON file for each origin resource
A json! path containing a Tera expression is deferred until rescile evaluates the rule. This lets
the current origin_resource select one of several input files without combining those files first.
Use template! or function! for intermediate values that depend on the current resource:
origin_resource = "service"
provider_name = { "function!" = "{{- origin_resource.name | regexp(expr='s/^([^-]+)-.*/\\1/') | lower -}}" }
roles = { "json!" = "{{- provider_name -}}-roles.json", jmespath = "values(roles)[?service == 'compute'] | [0]" }
[[create_resource]]
resource_type = "login"
relation_type = "DEFINED_BY"
name = "{{ origin_resource.name }}-login"
[create_resource.properties]
role_name = "{{ roles.name }}"
For aws-compute-production, rescile renders provider_name as aws, loads
data/input/aws-roles.json, applies the JMESPath expression, and binds the result to roles.
It repeats selection for each origin resource, but reads and parses each distinct file only once per
import run. The first read is the consistent snapshot used by later model stabilization iterations.
Dynamic JSON paths have these constraints:
- They are supported in model headers. Compliance and output files accept only literal
json!paths. - Rendered paths must stay below the input directory. Absolute paths, parent traversal (
..), and symlink escapes are rejected. - A missing file, invalid JSON, invalid JMESPath expression, template error, or dependency cycle fails the import instead of skipping the affected resource.
Use a dynamic JSON result with create_from.list
A dynamic JSON binding can supply create_from.list. Rescile infers the loop scope from the binding’s
transitive dependencies.
When the path uses only global context such as params, rescile resolves the file once and performs
the normal global list iteration:
providers = { "json!" = "{{ params.environment }}-providers.json", jmespath = "providers" }
[[create_resource]]
create_from = { list = "providers", as = "provider" }
name = "{{ value.name }}"
When the path depends on origin_resource, directly or through template! or function!, rescile
performs nested iteration:
origin_resource = "compute"
environment_name = { "function!" = "{{ origin_resource.environment | lower }}" }
providers = { "json!" = "{{ environment_name }}-providers.json", jmespath = "providers" }
[[create_resource]]
create_from = { list = "providers" }
resource_type = "login"
relation_type = "USES_PROVIDER"
name = "{{ origin_resource.name }}-{{ value.name }}-login"
The nested execution is equivalent to:
for each compute as origin_resource:
load providers for origin_resource.environment
for each providers item as value:
create login and its USES_PROVIDER relation
Both origin_resource and value are available inside the inner rule. Do not reference value in
the JSON path: rescile must load the list before it can establish value, so that dependency fails
the import as a cycle. If as and resource_type are both present, as determines the created
resource type, as with any other create_from rule.
Pre-filtering JSON with JMESPath
Add a jmespath key alongside json! to filter data before rescile binds it to the template
context. For literal paths this happens once during model parsing. For dynamic paths it happens
after the per-resource path is rendered; identical path-and-query combinations are cached for the
rest of the import run.
# Select only the first critical-level policy from slas.json
critical_sla = { "json!" = "slas.json", jmespath = "policies[?level == 'critical'] | [0]" }
[[create_resource]]
[create_resource.properties]
backup_frequency = "{{ critical_sla.backup_frequency }}"
On-demand HTTP and GraphQL inputs (external!)
Use external! when a model needs remote JSON for the current iteration. Unlike a Phase 0 generator,
rescile fetches only argument combinations reached by model rules. Fetch definitions live in the
module’s external/ directory and support HTTP GET, POST, GraphQL, pagination, vault secrets, and
multiple sequential steps.
# models/server.toml
origin_resource = "application"
vendor_data = { "external!" = "vendor_lookup", args = { vendor = "{{ origin_resource.vendor }}" }, jmespath = "data.vendor" }
[[create_resource]]
resource_type = "server"
name = "srv-{{ origin_resource.name }}"
[create_resource.properties]
support_url = "{{ vendor_data.supportUrl }}"
# external/vendor_lookup.toml
[external]
name = "vendor_lookup"
result_step = "lookup"
ttl = "1h"
[[external.steps]]
name = "lookup"
method = "POST"
url = "https://api.example.com/graphql"
graphql_query = '''
query Vendor($name: String!) {
vendor(name: $name) { supportUrl }
}
'''
graphql_variables = { name = "{{ args.vendor }}" }
Available fetcher template variables:
| Variable | Meaning |
|---|---|
args.<name> |
Arguments rendered from the current model iteration. |
params.<name> |
Module parameters. |
env.<name> |
Process environment variables. |
secrets.<name> |
Vault secrets declared by the fetcher. |
responses.<step>.json |
Parsed JSON from an earlier step. |
responses.<step>.text |
Raw body from an earlier step. |
responses.<step>.status |
HTTP status from an earlier step. |
responses.<step>.headers |
Headers from an earlier step. |
Calls are deduplicated by fetcher definition and rendered arguments. A cached result is reused until
its TTL expires. With --offline, rescile never sends an HTTP request: it uses an exact-key cached
result even when expired, or fails the build when none exists. HTTP failures, invalid JSON, and
invalid JMESPath filters are hard errors.
An external result can supply a create_from list after JMESPath selects an array:
origin_resource = ""
available_regions = { "external!" = "regions", args = { cloud = "{{ params.cloud }}" }, jmespath = "regions[*].name" }
[[create_resource]]
create_from = { list = "available_regions", as = "region" }
resource_type = "region"
name = "{{ value }}"
An external variable used as a create_from list is global to the model file. Its arguments cannot
reference origin_resource or value. External fetchers are read-only data sources: only the HTTP
runtime is accepted, and POST requests must not perform mutations or other side effects.
Importing Arbitrary JSON “As Is”
When JSON keys contain characters that violate GraphQL naming rules (e.g. netbox-2.10.0), rescile
exposes the value as a custom JSON scalar type, bypassing sanitization. Use json_encode | safe in
your template to pass the data through unchanged:
bundle_data = { "json!" = "bundle.json" }
[[output]]
resource_type = "bundle_list"
name = "bundles"
filename = "bundles.json"
mimetype = "application/json"
template = '{ "data": {{ bundle_data | json_encode | safe }} }'
Global vs. Iteration Variables
Header variables can be evaluated in two different contexts:
| Syntax | When evaluated | origin_resource is… |
|---|---|---|
"{{ ... }}" (plain string) |
Once globally, before the loop | A list of all matching resources |
{ "template!" = "..." } or { "function!" = "..." } |
Per iteration, inside the loop | The single resource being processed |
origin_resource = "network"
# Evaluated GLOBALLY — origin_resource is a list of ALL networks
total_network_count = "{{ origin_resource | length }}"
# Evaluated LOCALLY per iteration — origin_resource is the current network
subnet_count = { "function!" = '''
{%- set subnets = origin_resource.subnet | default(value=[]) -%}
{{ subnets | length }}
''' }
[[create_resource]]
[create_resource.properties]
total_subnets = "{{ subnet_count }}"
Module Parameters
Parameters passed via --module-params on the command line are available as {{ params.<name> }}
across all models/, compliance/, output/, and actions/ files in the module.
rescile-ce --module ./my-module --module-params "region=eu-central-1;env=prod" serve
# models/server.toml — inside the module
[create_resource.properties]
hostname = "srv-{{ params.region }}-{{ origin_resource.name }}"
For more on modules and parameters, see Using Modules.
2. Jsonnet
Jsonnet is a powerful data templating and functional configuration language. In Rescile, Jsonnet is a first-class peer to TOML, supported natively for:
- Full Functional Models (
models/*.jsonnet): Author entire model rule sets using functions, array comprehensions, multi-origin scoping, andlib/*.libsonnetimports. See Functional Models with Jsonnet. - Functional Compliance Audits (
compliance/*.jsonnet): Build compliance frameworks, audits, and control targets dynamically withrescile.audit()and comprehensions. - Functional Output Definitions (
output/*.jsonnet): Define output artifact rules using functional expressions. - Structured Output Payloads (
jsonnet = '''...'''): Generate complex, nested JSON documents (such as Kubernetes manifests, Terraform JSON, or OSCAL documents) without messy string escaping.
Output Body Generation with Jsonnet
In output/ files (whether .toml or .jsonnet), set jsonnet = """...""" instead of template = "..."
to construct the rendered output body natively:
For a complete reference on the output engine, see Output Engine.
Data Context in Jsonnet
Data is injected as external variables and accessed with std.extVar():
| Variable | How to access |
|---|---|
origin_resource |
std.extVar("origin_resource") |
| Any header variable | std.extVar("variable_name") |
Example — Inline Jsonnet Logic
origin_resource = "server"
[[output]]
resource_type = "k8s_manifest"
name = "svc-{{ origin_resource.name }}" # Name still uses Tera
filename = "svc-{{ origin_resource.name }}.json"
mimetype = "application/json"
jsonnet = """
local origin = std.extVar("origin_resource");
{
apiVersion: "v1",
kind: "Service",
metadata: {
name: origin.name,
labels: {
app: if std.length(origin.application) > 0
then origin.application[0].name
else "unknown"
}
},
spec: {
ports: [{ port: p } for p in origin.ports]
}
}
"""
Example — Modular Imports (.libsonnet)
rescile supports sibling imports: .libsonnet or .jsonnet files in the same directory as the
TOML file can be imported directly by filename.
data/output/k8s.libsonnet
{
service(name, ports):: {
apiVersion: "v1",
kind: "Service",
metadata: { name: name },
spec: { ports: [{ port: p } for p in ports] }
}
}
data/output/services.toml
origin_resource = "server"
[[output]]
resource_type = "k8s_manifest"
name = "svc-{{ origin_resource.name }}"
filename = "svc-{{ origin_resource.name }}.json"
mimetype = "application/json"
jsonnet = """
local k8s = import 'k8s.libsonnet';
local origin = std.extVar("origin_resource");
k8s.service(origin.name, origin.ports)
"""
Example — OSCAL Compliance
Jsonnet’s list comprehensions and helper functions make it well-suited for generating complex standards-based documents like OSCAL (Open Security Controls Assessment Language):
# data/output/audit_oscal.toml
origin_resource = "audit"
[[output]]
resource_type = "oscal_ssp"
name = "oscal-ssp-{{ origin_resource.name }}"
filename = "{{ origin_resource.name }}-ssp.json"
mimetype = "application/json"
jsonnet = '''
local origin = std.extVar("origin_resource");
{
"system-security-plan": {
metadata: {
title: "System Security Plan for " + origin.audit_name,
},
"control-implementation": {
"implemented-requirements": [
{
"control-id": ctrl.name,
description: std.get(ctrl, "evidence_summary", {}),
}
for ctrl in std.get(origin, "control", [])
],
},
},
}
'''
For a complete walkthrough of generating OSCAL and Markdown audit reports, see Generating Audit Artifacts.
3. JMESPath
JMESPath is a query language for JSON. In rescile it appears in two places:
| Usage | How |
|---|---|
| Header pre-filter | { "json!" = "file.json", jmespath = "..." } — filters JSON data once, before the loop |
| Tera filter | | jmespath(query="...") — queries a value inline inside any template |
Pre-filtering JSON at Load Time
Adding a jmespath or jp key alongside json! selects or reshapes data before it is bound to a
variable. This is more efficient than filtering the full dataset inside every rule block.
# Only load the first SLA policy whose level is "critical"
critical_sla = { "json!" = "slas.json", jmespath = "policies[?level == 'critical'] | [0]" }
[[create_resource]]
[create_resource.properties]
backup_frequency = "{{ critical_sla.backup_frequency }}"
rto_hours = "{{ critical_sla.rto_hours }}"
Querying Inline with the jmespath Filter
The jmespath Tera filter lets you run a JMESPath query against any JSON-compatible value at
template evaluation time. It returns the query result, or an empty/falsy value when nothing matches.
[create_resource.properties]
# origin_resource.config = { "storage": { "class": "premium-ssd" } }
storage_class = "{{ origin_resource.config | jmespath(query='storage.class') }}"
# → "premium-ssd"
Using JMESPath in match_on Expressions
Combine the expression operator with the jmespath filter to match on deeply nested JSON
data that the standard operators cannot reach:
[[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_for_{{ origin_resource.name }}"
The condition is true when the JMESPath query returns a non-empty, non-null result. For a dedicated guide to this pattern, see Matching on JSON with jmespath.
For the complete filter signature and more examples, see the
Tera Function & Filter Reference — jmespath
section of the Reference guide.
4. Choosing the Right Tool
| Scenario | Recommended tool |
|---|---|
| Simple string interpolation, property access, conditionals | Tera — {{ ... }} / {% ... %} |
| Arithmetic, loops, and array manipulations in property values | Tera filters (map, join, select, …) |
| Extracting a value from a nested JSON object | JMESPath filter inside Tera |
| Pre-selecting a slice of a large JSON file before the loop | JMESPath header pre-filter (jmespath =) |
| Generating a complete, structured JSON document (manifests, tfvars) | Jsonnet in [[output]] |
| Reusable JSON generation logic shared across multiple output files | Jsonnet with .libsonnet imports |
Quick Reference
Tera Variables Available in Rule Blocks
| Variable | Available when | Description |
|---|---|---|
origin_resource |
origin_resource is set in header |
The resource being processed in the current iteration |
value |
Inside a create_from block |
The current item from the iterated list or property |
origin_resource_counter |
origin_resource is set |
Zero-based counter, increments per rule block per iteration |
<header key> |
Always | Any top-level key defined in the file header |
params.<name> |
Module is loaded with --module-params |
Parameters passed via CLI, available globally |
rescile_endpoints |
Always (when serving) | Map containing controller, mcp, and vault URLs for the current environment. Can be overridden via RESCILE_CTX_URL_* env vars. |
RESOURCE_ALIASES |
Dependency aliasing is configured | Map of original → aliased resource type names |
env.<VAR> |
proxy.toml only |
Environment variables (CE: system env; Enterprise: workspace secrets) |
request |
proxy.toml inject_headers only |
Incoming request object (method, path, body, timestamp, …) |
response |
proxy.toml response_filter only |
The JSON payload returned from the proxied target API |
Accessing Linked Resources in Templates
A property that has been linked automatically (or via [[link_resources]]) is an array of node objects, even if only one target exists. Access its fields with an index or by collecting values:
security_group_name = "{{ origin_resource.security_group[0].name }}"
security_group_names = "{{ origin_resource.security_group | map(attribute='name') | join(sep=', ') }}"
Header Variable Evaluation Modes
origin_resource = "application"
# ── Evaluated ONCE globally (origin_resource = list of ALL applications) ──
total = "{{ origin_resource | length }}"
loaded = { "json!" = "data/ranges.json" }
# ── Evaluated PER ITERATION (origin_resource = the current application) ──
per_app = { "template!" = "{{ origin_resource.name | upper }}" }
# or equivalently:
per_app2 = { "function!" = "{{ origin_resource.name | upper }}" }
Commonly Used Tera Filters
| Filter | Example | Result |
|---|---|---|
upper / lower |
{{ name | upper }} |
"BILLING-API" |
replace(from, to) |
{{ name | replace(from=".", to="-") }} |
"billing-api" |
default(value) |
{{ x | default(value="n/a") }} |
fallback when undefined |
split(pat) + first |
{{ v | split(pat=".") | first }} |
major version component |
join(sep) |
{{ arr | join(sep=",") }} |
comma-separated string |
map(attribute) |
{{ servers | map(attribute="ip") }} |
array of IPs |
json_encode | safe |
{{ obj | json_encode | safe }} |
inline JSON (no escaping) |
length |
{{ items | length }} |
item count |
jmespath(query) |
{{ obj | jmespath(query="a.b") }} |
nested value extraction |
select(from, match) |
{{ list | select(from="name", match="status=active") }} |
filtered property list |
cidr_nth_subnet(prefix, nth) |
{{ "10.0.0.0/16" | cidr_nth_subnet(prefix=24, nth=0) }} |
"10.0.0.0/24" |
For the complete list of rescile-specific functions and filters — including counter,
calculate_cidr, allocate_subnets, sha256, base64_encode, and hmac_sha256 — see the
Tera Function & Filter Reference.