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
applicationnamedbilling-apiresults in a newservernamedbilling-api_server.(application:billing-api) -[HOSTED_ON]-> (server:billing-api_server)
This process can be visualized as follows:
application"]
B["[[create_resource]] ruleto 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
serviceresources are created (svc-MONITORING,svc-ALERTING) and linked to thezabbixapplication via aPROVIDESrelationship.
The logic of create_from_property is a powerful convention:
- It reads the property specified (e.g.,
service). - The name of the property (
service) becomes the type of the new resource(s). - It checks if the property’s value is a list (an array or a comma-separated string).
- 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 thenametemplate, where{{ property.value }}refers to the item being processed. - It creates the specified relationship from the origin resource to each new resource.
application resource with propertyservice: 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
Deriving from a Related Node
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
USESrelationship is created from thesubscriptionto theservice. Ifrelation_originwere omitted, the relationship would be created from theapplication.
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.
]
[[link_resources]]: Enriching Data from Across the Graph
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
subscriptionwithlocation: "fra", this rule finds thelocationresource withname: "fra"and copies itscomplianceproperty to thesubscription.
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_relationblock defines the new relationship. You can specify itstype,direction("local_to_remote"or"remote_to_local"), and even addproperties.
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:
- The importer identifies all resource types that were created by model files (e.g.,
server,service,classification). - It then scans all resources in the graph (both from assets and other models).
- 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.
- 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
serverresources fromapplicationresources. A server namedbilling-api_serveris created. -
data/assets/host.csvdefines physical hosts and has aservercolumn.# 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 eachorigin_resourceprocessed 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 basicorigin_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_relationobject 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.