Architectural Models

Transforming Raw Assets into a Rich Dependency Graph


2. Model Engine: Building Your Digital Twin (data/models/*.toml)

While Assets provide the raw data, the Model Engine is where you define the architecture. Models are declarative blueprints in TOML that transform asset data into a rich, contextual dependency graph. Each model file, located in data/models/, typically introduces a new type of resource by deriving it from an existing one.

File Header

The model engine provides a rich context of data that you can access within your templates, allowing for highly dynamic configurations. This data comes from three primary sources defined in the model file’s header.

# data/models/server.toml
# 1. The `origin_resource`: The primary data source for iteration.
#    This model will run once for every "application" resource.
origin_resource = "application"

# 2. Inline TOML Data: Static data defined directly in the model.
#    Useful for constants or configuration maps.
server_specs = { standard_cpu = 4, large_cpu = 8 }

# 3. External JSON Files: Data loaded from an external file.
#    Ideal for larger datasets managed separately.
network_data = { "json!" = "data/networks.json" }
Key Description
origin_resource Optional. The source resource type this model’s rules will iterate over. Every rule ([[create_resource]], etc.) will execute once for each resource of this type. If this key is omitted, rules execute only once in a global context, without an origin_resource to reference.
(any name) Optional. Any other top-level key is treated as custom data available to templates. This can be an inline TOML table (like server_specs) or a reference to an external JSON file using the special { "json!" = "path/to/file.json" } syntax.

This data is then accessible within templates, for example: {{ origin_resource.name }}, {{ server_specs.standard_cpu }}, or {{ network_data.subnets.prod }}.

Creating Derived Resources: [[create_resource]]

This is the primary directive for creating new resources. It can be used unconditionally or conditionally based on the properties of the origin_resource.

Unconditional Creation

This rule creates one new resource for every origin_resource.

Goal: For every application, create a corresponding server resource.

# The model reads resources of type "application".
origin_resource = "application"

server_specs = { standard_cpu = 4, large_cpu = 8 }

[[create_resource]]
resource_type = "server"
relation_type = "HOSTED_ON"
name = "{{origin_resource.name}}_server"
[create_resource.properties]
os = "Linux"
cpu_cores = "{{ server_specs.standard_cpu }}" # Access data from header
managed_by = "{{origin_resource.owner}}"   # Inherit property from application

The [[create_resource]] block has several powerful options:

Key Mandatory Description
match_on No An array of filter objects. The rule applies if all conditions match.
resource_type Yes* The type for the new resource. (*Unless using create_from_property.)
relation_type Yes The type for the relationship connecting the origin resource to the new resource.
name Yes* A template for the primary key of the new resource. (*Unless using create_from_property.)
properties No A key-value map of properties to add to the new resource. Values support templates.
relation_properties No A key-value map of properties to add to the new relationship. Values support templates.
create_from_property No Creates a resource using a property’s name for the type and its value for the name.
property_origin No When using create_from_property, specifies a related resource type to read the property from.
relation_origin No When using create_from_property, controls the new relationship’s origin ("origin_resource" or "property_origin").

relation_properties: You can also add properties to the newly created relationship using a [create_resource.relation_properties] block.

```toml
[[create_resource]]
resource_type = "server"
relation_type = "HOSTED_ON"
name = "{{origin_resource.name}}_server"
[create_resource.properties]
# ... server properties
[create_resource.relation_properties]
source_app_env = "{{ origin_resource.environment }}"
managed = true
```
  • Result: An application named billing-api results in a new server named billing-api_server. (application:billing-api) -[HOSTED_ON]-> (server:billing-api_server)

This process can be visualized as follows:

graph TD subgraph "Input & Rules" A["Origin Resource: application"] B["[[create_resource]] rule
to create a server"] end subgraph "rescile Process" C{"For each application..."} D["1. Render name, properties, and relation_properties from templates"] E["2. Create new server resource"] F["3. Create HOSTED_ON relationship with its properties"] end subgraph "Output Graph" G(application: billing-api) H(server: billing-api_server) G -- "HOSTED_ON
{ source_app_env: 'prod', ... }" --> H end A & B --> C --> D --> E --> F F --> G & H

Conditional Creation with match_on

You can apply rules selectively using a match_on block. All conditions in the block must be true (AND logic).

# In data/models/classification.toml
origin_resource = "application"

[[create_resource]]
match_on = [
  { property = "environment", value = "prod" }
]
resource_type = "classification"
relation_type = "IS_CLASSIFIED_AS"
name = "{{origin_resource.name}}_prod"
[create_resource.properties]
level = "Restricted"

The match_on block supports several operators:

Operator Type Description
property String Mandatory. The name of the property on the source resource to match against.
value String, Bool, Num Performs an exact, type-aware match on the property’s value.
not String, Bool, Num The condition is true if the property’s value is not equal to this value.
contains String For a string property, checks if it contains the substring. For an array property, checks if it contains this value as an element.
exists Boolean If true, the condition is true if the property exists and is not null/empty. If false, it’s true if the property does not exist or is null/empty.
empty Boolean If true, the condition is true if the property is missing, null, an empty string (""), or an empty array ([]). false if it has a value.
greater Number For a numeric property, checks if its value is greater than this number (>).
lower Number For a numeric property, checks if its value is less than this number (<).

To express OR logic, use a nested or block:

origin_resource = "server"

match_on = [
  { property = "status", value = "active" }, # Must be active AND...
  { or = [ [ { property = "cores", greater = 8 } ], [ { property = "legacy_system", value = "false" } ] ] } # (...have >8 cores OR not be a legacy system)
]

Convention-Over-Configuration with create_from_property

This directive creates new resources by convention, using a property’s name as the new resource’s type and its value as the primary key. If the property contains an array or comma-separated string, it creates a resource for each item.

Goal: From an application’s service property, create service resources.

data/assets/application.csv

name,service
zabbix,"monitoring, alerting"

data/models/application_services.toml

origin_resource = "application"

[[create_resource]]
create_from_property = "service"
relation_type = "PROVIDES"
# You can optionally customize the name and properties
name = "svc-{{ property.value | upper }}" # `property.value` refers to the item being processed
[create_resource.properties]
category = "Operations"
  • Result: Two service resources are created (svc-MONITORING, svc-ALERTING) and linked to the zabbix application via a PROVIDES relationship.

The logic of create_from_property is a powerful convention:

  1. It reads the property specified (e.g., service).
  2. The name of the property (service) becomes the type of the new resource(s).
  3. It checks if the property’s value is a list (an array or a comma-separated string).
  4. For each item in the list, it uses the item’s value ("monitoring") as the primary key for the new resource. This can be customized with the name template, where {{ property.value }} refers to the item being processed.
  5. It creates the specified relationship from the origin resource to each new resource.
graph TD subgraph "Input" A["application resource with property
service: monitoring, alerting"] end subgraph "rescile Process" B["1. Read service property"] C["2. Split value into [monitoring, alerting]"] D{"3. For each item..."} E["→ monitoring: Create service resource named monitoring"] F["→ alerting: Create service resource named alerting"] end subgraph "Output Graph" H(application) -- PROVIDES --> I(service: monitoring) H -- PROVIDES --> J(service: alerting) end A --> B --> C --> D --> E & F --> H

You can also derive resources from a property on a related node using property_origin and control the new relationship’s starting point with relation_origin.

Goal: From a subscription, find its related application, read the service property, and create a service resource linked back to the original subscription.

# In data/models/subscription_services.toml
origin_resource = "subscription"

[[create_resource]]
property_origin = "application"     # 1. Look on the related 'application' node.
create_from_property = "service"    # 2. Use its 'service' property.
relation_type = "USES"              # 3. The new relation's type.
relation_origin = "origin_resource" # 4. Start the new relation from the 'subscription'.
  • Result: A USES relationship is created from the subscription to the service. If relation_origin were omitted, the relationship would be created from the application.

Propagating and Linking Data

[[copy_property]]: Propagating Data to Connected Nodes

This directive “pushes” properties from an origin_resource to a directly connected resource. It’s a clean, declarative way to propagate context through the graph.

Goal: For every application, copy its network property to its connected subscription resources.

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

[[copy_property]]
to = "subscription" # The type of the connected resource to copy properties TO.
properties = [
  "network",                                      # Copies the 'network' property with the same name.
  { from = "owner", as = "subscription_owner" }   # Copies 'owner' and renames it.
]

This powerful directive performs a “join” across the graph to enrich a resource. It finds another, potentially unrelated, resource based on matching property values and then performs actions like creating a relationship or copying properties.

Action: copy_properties

This is used to “pull” data from another resource. A common pattern is to enrich a resource with data from a central “lookup” table, like a location.

Goal: For every subscription, find the location resource that matches its location property and copy the compliance framework from it.

# In data/models/subscription.toml
origin_resource = "subscription"

[[link_resources]]
with = "location" # The type of the "remote" resource to join with.
on = { local = "location", remote = "name" } # The join condition.
copy_properties = [ "compliance" ]
  • Graph Impact: For a subscription with location: "fra", this rule finds the location resource with name: "fra" and copies its compliance property to the subscription.
Action: create_relation

You can also use link_resources to create a new relationship between the joined resources.

Goal: Join control resources with security_plan resources where their key properties match, and create a DEFINED_BY relationship between them.

# In data/models/control.toml
origin_resource = "control"

[[link_resources]]
with = "security_plan"
on = { local = "control_id", remote = "plan_id" }
create_relation = { type = "DEFINED_BY" } # Creates relation: (control)-[DEFINED_BY]->(security_plan)
  • The create_relation block defines the new relationship. You can specify its type, direction ("local_to_remote" or "remote_to_local"), and even add properties.
Singleton Join (and Multiple Actions)

By omitting the on key, you can join every origin_resource to the single instance of the with resource. This is useful for linking all components to a global configuration object. You can perform multiple actions in one block.

Goal: Link every domain to the single subscription resource. Create a PART_OF relationship from the subscription to the domain, and copy the subscription’s name to a new property on the domain.

# origin_resource = "domain"
[[link_resources]]
with = "subscription" # Omitting 'on' declares a singleton join.

# Actions to perform for each domain:
# 1. Create a relation from the 'subscription' (remote) to the 'domain' (local).
create_relation = { type = "PART_OF", direction = "remote_to_local" } # (subscription) -> (domain)
# 2. Copy 'name' from 'subscription' to a new 'subscription_name' property on the 'domain'.
copy_properties = [ { from = "name", as = "subscription_name" } ]
# origin_resource = "domain"
[[link_resources]]
with = "subscription" # Omitting 'on' declares a singleton join.

# Actions to perform for each domain:
copy_properties = [ { from = "name", as = "subscription_name" } ]

Automatic Relationship Creation for Derived Resources

After all architectural models ([[create_resource]], etc.) have been processed, rescile performs an automatic relationship creation phase. This is similar to the initial asset-to-asset relationship creation but targets resources created by models.

The convention is as follows:

  1. The importer identifies all resource types that were created by model files (e.g., server, service, classification).
  2. It then scans all resources in the graph (both from assets and other models).
  3. If a resource has a property whose key matches the name of a model-defined resource type, the importer treats the property’s value as a primary key.
  4. It then creates a relationship from the resource to the corresponding model-defined resource.

This allows for a powerful, convention-based way to link resources without writing explicit rules for every connection.

Example: Linking a host asset to a model-defined server

  • A model file creates server resources from application resources. A server named billing-api_server is created.

  • data/assets/host.csv defines physical hosts and has a server column.

    # data/assets/host.csv
    name,ip_address,server
    host-01,10.0.1.10,billing-api_server
    

During this phase, the importer sees that host-01 has a property server. Since server is a known model-defined resource type, it creates a relationship: (host:host-01) -[server]-> (server:billing-api_server).

Preventing Automatic Linking

In some cases, a property name might coincidentally match a resource type, leading to an unwanted relationship. To prevent a property from being used for automatic linking, prefix its name with an underscore (_) in your model file’s [create_resource.properties] block or in your asset CSV header. The importer will create the property on the resource without the leading underscore but will exclude it from the auto-linking process.

[create_resource.properties]
# The `service` property will be created on the new resource, but `rescile`
# will not attempt to create a relationship from it to a `service` resource.
_service = "{{ origin_resource.service_name }}"

Templating with Tera

All string values in name, properties, and match_on are processed by the Tera templating engine, enabling dynamic logic.

Accessing Data

You can access several types of data within your templates:

  • origin_resource: An object containing all properties of the resource being processed.
  • origin_resource_counter: A zero-based counter that increments for each origin_resource processed by a [[create_resource]] rule. It’s useful for creating unique, numbered resources.
  • counter(key) function: A stateful, keyed counter for advanced numbering scenarios. For example, counter(key=origin_resource.environment) would maintain a separate count for “prod”, “dev”, etc., which is more flexible than the basic origin_resource_counter.
  • Custom Data: Any inline TOML tables or external JSON data defined in the file header.
  • Related Resources: When traversing a relationship (e.g., origin_resource.database[0]), the target resource’s properties are available, along with a special _relation object containing the properties of the connecting edge.

You can also perform more complex data transformations, such as collecting attributes from related nodes.

[create_resource.properties]
resource_index = "{{ origin_resource_counter }}" # Simple index for this rule
env_index = "{{ counter(key=origin_resource.environment) }}" # Keyed index, e.g., 0 for 'prod', 0 for 'dev', 1 for 'prod', etc.
hostname = "{{ origin_resource.name | upper | replace(from='.', to='-') }}" # Chained filters
sla_level = "{% if origin_resource.environment == 'prod' %}Gold{% else %}Silver{% endif %}" # Conditional logic
db_connection_type = "{{ origin_resource.database[0]._relation.label }}" # Access edge properties
# Collect all 'volume' attributes from related 'application' nodes into a JSON string.
volumes = "{{ origin_resource.application | map(attribute='volume') | json_encode }}"

For more details on advanced templating, see the Advanced Modeling documentation.