Advanced Modeling
Mastering Conditional Logic, Templating, and Declarative Patterns
5. Advanced Modeling Techniques
Once you’re comfortable with the basics of assets, models, and compliance, you can leverage rescile’s more
advanced features to model complex logic and handle enterprise-scale scenarios declaratively.
Advanced Conditional Logic with match_on
The match_on key is used in both models and compliance files to apply rules conditionally. While simple equality
checks are common, it supports a range of operators and complex logical structures.
Match Operators
A match_on block is an array of condition objects. All conditions in the array must be true for the rule to apply
(AND logic). Each condition object can use one of the following operators:
| Key | Type | Description |
|---|---|---|
property |
String | Mandatory. The name of the property to match against. |
value |
String, Bool, Num | Performs an exact match. Supports Tera templating. |
not |
String, Bool, Num | Matches if the property’s value is not equal to this. Supports Tera templating. |
contains |
String | For string properties, checks for a substring. For array properties, checks if the array contains this value as an element. Supports Tera templating. |
exists |
Boolean | If true, matches if the property exists and is not empty/null. If false, matches if it does not exist or is empty/null. |
empty |
Boolean | If true, matches if the property is missing, null, an empty string (""), or an empty array ([]). |
greater |
Number | For numeric properties, checks if the value is > this number. |
lower |
Number | For numeric properties, checks if the value is < this number. |
Dynamic Matching with Templating
The value, not, and contains operators support templating, allowing you to create dynamic, context-aware match conditions. This is extremely powerful for creating data-driven relationships where the target of a link is determined by a property on the source.
Goal: For every application, find the provider resource whose name is stored in the application’s maintainer property and create a HAS_RESPONSIBILITY link to it.
data/compliance/ownership_linking.toml
audit_id = "INTERNAL-OWNERSHIP"
audit_name = "Internal"
[[control]]
id = "OWNERSHIP-LINK-1"
name = "Link Applications to Maintainers"
# ...
[[control.target]]
# 1. Find all application resources.
origin_resource_type = "application"
# 2. This block defines the target 'provider' resource to link to.
[control.target.resource]
type = "provider"
# 3. The 'match_on' block finds the *single* provider to link to.
# The `value` is dynamically rendered for each application.
match_on = [
{ property = "name", value = "{{ origin_resource.maintainer }}" }
]
# 4. This block defines the new relationship to create.
[control.target.relation]
type = "HAS_RESPONSIBILITY"
As the importer processes each application, it renders the {{ origin_resource.maintainer }} template. For an application with maintainer: "team-alpha", the rule will search for a provider resource where name == "team-alpha" and create the link. This powerful pattern avoids hardcoding names and makes your compliance logic data-driven.
Complex Logic with or
To model OR conditions, you can use a special or block inside the match_on array. The structure
is { or = [ […], […] ] }, where each inner array is an AND-group, and the outer array connects them with OR logic.
Goal: Create a resource if an application is active AND (it has more than 8 cores OR it is not a
legacy_system).
data/models/advanced_sla.toml
origin_resource = "application"
[[create_resource]]
match_on = [
# First top-level AND condition
{ property = "status", value = "active" },
# Second top-level AND condition, which contains an OR block
{ or = [
[ { property = "cores", greater = 8 } ], # First OR case
[ { property = "legacy_system", value = "false" } ] # Second OR case
]
}
]
resource_type = "sla"
relation_type = "HAS_SLA"
name = "gold_sla_for_{{origin_resource.name}}"
[create_resource.properties]
level = "Gold"
This powerful combination of AND and OR logic allows you to model highly specific architectural and compliance rules declaratively.
Dynamic Properties with Templating
rescile uses the powerful Tera templating engine, which allows for much more than simple variable replacement.
You can use logic, filters, and loops directly within your TOML string values.
Accessing Resource Properties
As seen in previous examples, you can use dot notation to access properties of the origin_resource being processed. This extends to traversing relationships. If an origin_resource is linked to another resource (e.g., an application is linked to a database), you can access properties of the related resource.
rescile also provides a special origin_resource_counter variable. This is a zero-based counter that increments for each origin_resource processed by a [[create_resource]] rule. It is unique per rule block and allows you to create predictably numbered resources or properties.
If the relationship is one-to-many, rescile makes the related resources available as an array of objects. You can access specific items by index (e.g., [0]) or iterate over them using a for loop or filters.
name = "{{origin_resource.name}}_server"
[create_resource.properties]
managed_by = "{{origin_resource.owner}}"
# Access the 'version' property of the first related 'database'
db_version = "{{ origin_resource.database[0].version }}"
# Access a property from the edge connecting the two resources
connection_type = "{{ origin_resource.database[0]._relation.name }}"
Using External or Inline Data
You can define data directly in your TOML file or load it from an external JSON file using the
json!("path/to/file.json") syntax. This is useful for providing configuration maps or complex data structures to your
templates.
Any top-level table in your TOML file that isn’t a recognized directive (like create_resource) is treated as
injectable data.
data/models/classification.toml
origin_resource = "subscription"
# Load complex details from an external JSON file.
# The data will be available in templates as 'policy_details'.
policy_details = { "json!" = "data/policies/classification_rules.json" }
# This is an inline TOML table, available in templates as 'classification_levels'.
classification_levels = { prod = "Restricted", dev = "Internal" }
[[create_resource]]
match_on = [ { property = "environment", value = "prod" } ]
resource_type = "classification"
relation_type = "IS_CLASSIFIED_AS"
name = "classification_for_{{origin_resource.name}}"
[create_resource.properties]
# Access the inline table.
level = "{{ classification_levels.prod }}"
# Access a nested value from the external JSON file.
handling_instructions = "{{ policy_details.Restricted.handling }}"
Using Filters
Tera provides a wide range of built-in filters to manipulate data. You can chain them using the | pipe character. A full list is available in the Tera documentation.
[create_resource.properties]
# Convert a property to uppercase
hostname_prefix = "{{ origin_resource.name | upper }}"
# Replace characters in a string
sanitized_name = "{{ origin_resource.name | replace(from=\".\", to=\"-\") }}"
# Map an array of related 'application' objects to just their 'volume' properties,
# then encode the resulting array as a JSON string.
volumes = "{{ origin_resource.application | map(attribute=volume) | json_encode }}"
Conditional Logic and Loops
You can use {% if ... %} blocks for conditional logic and {% for ... %} for loops. This is extremely powerful for creating dynamic properties.
[create_resource.properties]
# Set a property based on another property's value
sla_level = "{% if origin_resource.environment == 'prod' %}Gold{% else %}Silver{% endif %}"
# Create a comma-separated list of formatted tags from an array property
tag_list = "{% for tag in origin_resource.tags %}{{ tag | upper }}{% if not loop.last %},{% endif %}{% endfor %}"
Simplyfing models using Tera templating:
Before two create_resource to distinguish the domain:
origin_resource = "service"
[[create_resource]]
match_on = [
{ property = "status", value = "deployed" },
{ property = "domain", exists = true }
]
resource_type = "service"
name = "{{origin_resource.service}}"
relation_type = "_GENERATED_FQDN"
[create_resource.properties]
url = "{{origin_resource.service_name}}.{{origin_resource.domain}}"
status = "dns_record_generated"
[[create_resource]]
match_on = [
{ property = "status", value = "deployed" },
{ property = "domain", exists = false }
]
resource_type = "service"
name = "{{origin_resource.service}}"
relation_type = "_GENERATED_FQDN"
[create_resource.properties]
url = "{{origin_resource.service_name}}.internal.local"
status = "dns_record_generated"
Using tera templating:
origin_resource = "service"
[[create_resource]]
match_on = [ { property = "status", value = "deployed" } ]
resource_type = "service"
name = "{{origin_resource.service}}"
relation_type = "_GENERATED_FQDN"
[create_resource.properties]
url = "{{ origin_resource.service_name }}.{{ origin_resource.domain | default(value='internal.local') }}"
status = "dns_record_generated"
Custom Functions and Filters
rescile extends Tera with custom functions and filters, particularly for network modeling. These can be used in any templated string value.
counter Function
A stateful counter that increments for each unique key provided. While rescile provides a basic origin_resource_counter variable for simple iteration, the counter function is much more powerful for scenarios where you need independent, keyed counters. For example, numbering resources within different groups (like subnets within different VPCs). The counters are reset for each importer run.
key: A string that uniquely identifies the counter. Each unique key will have its own independent, zero-based counter. This can be a static string or a dynamic value from a template.
Goal: For each department resource that belongs to a specific region, allocate a unique /24 subnet from the region’s larger /16 block. The numbering of subnets (nth) should restart from 0 for each region.
data/models/department_subnets.toml
origin_resource = "department"
[[create_resource]]
resource_type = "subnet"
relation_type = "HAS_SUBNET"
[create_resource.properties]
# The key for the counter is the region's CIDR block.
# For all departments in the '10.0.0.0/16' region, the counter will go 0, 1, 2...
# For all departments in the '10.1.0.0/16' region, it will also go 0, 1, 2...
subnet_cidr = "{{ origin_resource.region[0].cidr | cidr_nth_subnet(prefix=24, nth=counter(key=origin_resource.region[0].cidr)) }}"
cidr_nth_subnet Filter
Calculates a subnet within a parent CIDR block. This is useful for partitioning a large network block into smaller, predictable subnets.
prefix: The additional prefix to add to the parent CIDR.nth: The zero-based index of the subnet to select.
Goal: For each department resource, allocate a unique /24 subnet from a larger /16 block in a predictable, ordered fashion.
data/models/department_subnets.toml
origin_resource = "department"
[[create_resource]]
resource_type = "subnet"
relation_type = "HAS_SUBNET"
[create_resource.properties]
# For the first department, `origin_resource_counter` is 0, calculating 10.0.0.0/24.
# For the second, it's 1, calculating 10.0.1.0/24, and so on.
subnet_cidr = "{{ '10.0.0.0/16' | cidr_nth_subnet(prefix=24, nth=origin_resource_counter) }}"
allocate_subnets Function
A powerful function for dynamic subnet planning. It takes a parent CIDR block and a map of subnet names to their required host counts. It then calculates the smallest possible subnet for each request and allocates them sequentially within the parent block.
cidr: The parent CIDR block from which to allocate subnets.host_map: A dictionary where keys are the desired subnet names and values are the number of hosts required.
Goal: Automatically plan a network layout based on the needs of different environments.
data/models/network_layout.toml
origin_resource = "network"
# This is an inline TOML table, available in templates as 'required_subnets'.
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]
# The function allocates subnets from the network's main CIDR block.
# The keys are automatically sorted for deterministic allocation (app, db, dmz).
allocated_cidrs = "{{ allocate_subnets(cidr=origin_resource.cidr, host_map=required_subnets) }}"
Assuming origin_resource.cidr is "10.0.0.0/16", the allocated_cidrs property would become a JSON object string like this:
{
"app": "10.0.0.0/25",
"db": "10.0.0.128/26",
"dmz": "10.0.0.192/28"
}
This object can then be used in subsequent models to create the actual subnet resources.
Declarative Ownership with map
For enterprise-scale ownership and responsibility modeling, rescile provides an advanced, convention-based pattern
in compliance files: [control.target.map].
Instead of writing dozens of rules to link assets to owners, maintainers, and data processors, you can define the convention in a single block.
data/compliance/ownership.toml
[[control]]
id = "OWN-1"
name = "Map Asset Roles to Providers"
[[control.target]]
[control.target.map]
# 1. Find all resources of this type to get the list of possible roles (e.g., "owner", "maintainer").
derived_type = "role"
# 2. Scan these asset types for properties that match the role names.
origin_resource_types = ["application", "database"]
# 3. The value of the property is the name of a resource of this type.
target_resource_type = "provider"
# 4. Create a relation with this type from the asset to the provider.
primary_relation_type = "HAS_RESPONSIBILITY"
# 5. Add a property to the new relation indicating which role it represents.
property_on_relation = "role"
# 6. (Optional) Create a second link from the role definition itself to the provider.
link_relation_to_target = true
secondary_relation_type = "IS_PERFORMED_BY"
# 7. Use overrides to map properties like 'vendor' to the 'maintainer' role.
[control.target.map.property_map_overrides]
vendor = "maintainer"
This single rule scans hundreds of assets. If it finds an application with a property vendor = "team-alpha", it
performs two actions:
- It creates the primary responsibility link:
(application) -[HAS_RESPONSIBILITY {role: "maintainer"}]-> (provider:team-alpha). - Because
link_relation_to_targetistrue, it also creates a secondary link modeling which provider performs the role:(role:maintainer) -[IS_PERFORMED_BY]-> (provider:team-alpha).
vendor: team-alpha"] -- "HAS_RESPONSIBILITY
{role: maintainer}" --> Provider["provider
team-alpha"] end subgraph "Secondary Role Performance Link" Role["role
maintainer"] -- "IS_PERFORMED_BY" --> Provider end
This pattern is the key to managing organizational responsibility at scale.
Advanced Patterns and Use Cases
Once you have mastered the advanced syntax, you can model complex, large-scale enterprise environments by combining these features into powerful patterns:
-
FinOps Cost Attribution: Use a
link_resourcesrule to join cost data from a cloud billing export (billing_data.csv) onto thousands ofserverresources using a commonresource_id. This adds amonthly_costproperty, enabling queries like, “Show the total monthly cost of all infrastructure owned byteam-alpha.” -
Hardware Lifecycle Management: Use
link_resourcesto join server data from your CMDB with a separate hardware asset system using a commonserial_number. This enriches server resources with properties likeend_of_life_date, enabling proactive queries such as, “Show all applications running on hardware that will be unsupported in the next six months.” -
Disaster Recovery (DR) Twinning: For a policy that every production database in
aws-eu-central-1must have a replica inaws-eu-west-1, a rule can automatically enforce this. It would match on production databases in the primary region and create a correspondingdatabase_replicaresource in the DR region, linked by aREPLICATES_TOrelation. -
PCI-DSS Network Segmentation: Define a compliance rule that finds every connection from a non-PCI network zone to a PCI database zone. The rule then uses the “insert resource” pattern to automatically place an audited
pci_firewallresource into the path, making your topology auditable by design. -
Software Bill of Materials (SBOM) Vulnerability Tracing: Combine asset data with security intelligence for rapid impact analysis. When a new
log4jvulnerability is discovered, a single GraphQL query traversing from aninstalled_packageresource up to itsserverandapplicationcan instantly identify every affected application and its owner.
log4j vulnerability] --> IP[installed_package
log4j-core-2.14.1.jar] IP -- "INSTALLED_ON" --> S[server
app-server-01] S -- "RUNS" --> A[application
billing-api] A -- "OWNED_BY" --> O[owner
team-alpha] end
By defining your architecture, compliance rules, and ownership models in version-controlled files, you create a single, queryable source of truth that scales with your organization, ensuring consistency, auditability, and deep insight into your entire hybrid estate.