Architectural Models

Templating & Data

How to use Tera, Jsonnet, and JMESPath in rescile models, compliance, and output files — covering data sources, global vs. iteration variables, and module parameters.

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:

  1. Full Functional Models (models/*.jsonnet): Author entire model rule sets using functions, array comprehensions, multi-origin scoping, and lib/*.libsonnet imports. See Functional Models with Jsonnet.
  2. Functional Compliance Audits (compliance/*.jsonnet): Build compliance frameworks, audits, and control targets dynamically with rescile.audit() and comprehensions.
  3. Functional Output Definitions (output/*.jsonnet): Define output artifact rules using functional expressions.
  4. 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.