Architectural Models

The Iteration Pattern

Explains origin_resource blocks, rescile's chunked TOML parsing, rule iteration, and data-driven model patterns.

The Iteration Pattern

The origin_resource For-Each Loop and Anatomy of a Configuration File

The most important concept for writing model files is rescile’s implicit iteration pattern. Think of each origin_resource block as a declarative loop.

The Golden Rule: A block beginning with origin_resource = "application" is a for-each loop. It tells rescile to run the following rules once for every application in your graph, until another column-zero origin_resource declaration starts a new block.

You don’t write imperative loops — you declare a set of rules that operate on a collection of resources. rescile handles the iteration automatically.

How rescile parses model TOML

Model files support more than one origin block. Before TOML deserialization, rescile scans the raw file and splits it at every line matching ^origin_resource\s*=. Each chunk is then parsed independently as a model definition.

origin_resource = "application"

[[create_resource]]
resource_type = "server"
relation_type = "RUNS_ON"
name = "server-{{ origin_resource.name }}"

[create_resource.properties]
environment = "{{ origin_resource.environment }}"

origin_resource = "server"

[[link_resources]]
with = "subscription"
join = { local = "subscription_name", remote = "name" }
create_relation = { type = "BELONGS_TO" }

This file produces two model definitions: the first runs with application; the second runs with server.

A generic TOML parser applied to the complete file gives a different and misleading structure. Under standard whole-file TOML table rules, the second declaration appears below create_resource[0].properties. Rescile does not execute that whole-file parse tree because it splits the file first.

Requirements:

  • Every intended model origin_resource declaration must begin at column zero.
  • Do not indent the declaration. Leading spaces or tabs prevent rescile from recognizing a new chunk.
  • Do not infer execution scope from a generic whole-file TOML parse tree.
  • Use rescile-ce validate --strict --build, debug-build output, and saved-graph assertions to verify actual origins and relations.

Using one origin per file remains a reasonable readability convention, but it is not required for correct execution. Split files when clearer ownership is useful, not because multiple recognized origin blocks are invalid.

Declarative vs. Imperative

The declarative model file below is equivalent to the imperative pseudo-code that follows it.

# data/models/server.toml
origin_resource = "application"

[[create_resource]]
resource_type = "server"
relation_type = "HOSTED_ON"
name = "{{ origin_resource.name }}_server"

[[create_resource]]
match_on = [{ property = "environment", value = "prod" }]
resource_type = "monitoring_agent"
name = "agent_for_{{ origin_resource.name }}"
all_applications = graph.find_resources(type="application")

for current_application in all_applications:
    create_server_for(current_application)

    if current_application.environment == "prod":
        create_monitoring_agent_for(current_application)

Data-Driven Iteration with create_from

As an alternative to iterating over graph resources, rescile supports a data-driven iteration model using the create_from directive. This lets you generate resources from an abstract list defined in the file header.

# data/models/providers.toml
# No origin_resource is defined.

providers = { "json!" = "shared/providers.json" }
provider_list = { "template!" = '''{{ providers | map(attribute="name") | safe }}''' }

[[create_resource]]
create_from = { list = "provider_list", as = "provider" }
name = "ext-{{ value }}"

With a globally resolved header list, origin_resource is unavailable and the special value variable holds the current list item. A templated json! list that transitively depends on origin_resource instead creates a nested loop where both variables are available. See Templating and Data Manipulation.

Anatomy of a Model

A single-origin model commonly has a Header and one or more Rule Blocks. A multi-origin model repeats the origin header and rule blocks for each chunk. File-scoped data can be shared across those chunks.

# ── HEADER ──────────────────────────────────────────────────────────────
origin_resource = "application"   # The for-each subject

# File-scoped data available to all Rule Blocks
server_specs    = { standard_cpu = 4, premium_cpu = 8 }
ip_ranges       = { "json!" = "shared/ip_ranges.json" }

# ── RULE BLOCK 1 ─────────────────────────────────────────────────────────
[[create_resource]]
resource_type = "server"
relation_type = "HOSTS"
name = "server_{{ origin_resource.name }}"

[create_resource.properties]
cpu_cores = "{{ server_specs.standard_cpu }}"
status    = "provisioning"

# ── RULE BLOCK 2 ─────────────────────────────────────────────────────────
[[create_resource]]
match_on = [{ property = "environment", value = "prod" }]
resource_type = "monitoring_agent"
name = "agent_{{ origin_resource.name }}"

Header and origin scope

Key Description
origin_resource Optional. The resource type this chunk iterates over. A new column-zero declaration starts another chunk. If omitted, rules run once in a global context (useful for singletons or create_from generation).
Any other key File-scoped data available as template variables. May be an inline TOML table, a { "json!" = "..." } reference, or a { "template!" = "..." } or { "function!" = "..." } snippet.

When origin_resource is omitted, rules are processed exactly once unless create_from with a list is used. The origin_resource variable is not available in templates.

Rule Blocks and Directives

A Rule Block is a TOML array-of-tables entry such as [[create_resource]] or [[link_resources]]. Think of each block as a single executable function that rescile evaluates for every iteration of the loop.

Inside a Rule Block, Directives are the key-value pairs that configure the function — for example resource_type, name, relation_type, and match_on.

Global Context: Module Parameters

In addition to the file-scoped data defined in the header, any module parameters passed via --module-params on the command line are available as global variables in every template across all files in the module.

rescile-ce --module ./my-module --module-params "region=eu-central-1;env=prod" serve

Inside any template: {{ region }} renders "eu-central-1" and {{ env }} renders "prod".

For the complete reference on all available template data and functions, see Tera Essentials.