Linking Resources
Using
[[link_resources]]: A “Pull” Operation
The [[link_resources]] directive is a powerful “pull” operation, analogous to a database JOIN or a lookup function. It enriches the origin_resource by finding a remote resource based on a join condition and pulling data from it.
Unlike [[copy_property]] (a “push” operation that requires an existing connection), [[link_resources]] actively searches for the remote resource. It is the primary tool for enriching a resource with data from a central “lookup” table (e.g., joining a server with a central location database) or for creating explicit relationships based on shared keys.
How [[link_resources]] Works
| Key | Mandatory | Description |
|---|---|---|
with |
Yes | The type of the “remote” resource to find and join with. |
join |
No | A table defining the join condition: { local = "prop_on_origin", remote = "prop_on_with" }. If property names are the same, a simple string can be used. |
copy_properties |
No | An array defining properties to copy between the origin_resource and the with resource. Supports simple strings, rename syntax ({ from = "...", as = "..." }), transformation with a Tera template, and an explicit direction (see Copy direction). The engine first tries to copy a property from the origin_resource to the with resource; only if the origin does not have that property does it fall back to copying from the with resource to the origin. |
cardinality |
No | Expected target cardinality: "one" or "many" (default: "many"). When set to "one", more than one join-matched target emits a warning and skips copying to protect scalar properties. |
create_relation |
No | A table { type = "RELATION_TYPE" } to create a new relationship from the origin_resource to the with resource. |
match_on |
No | An array of filter objects to apply to the origin_resource set. |
match_with |
No | An array of filter objects to apply to the with resource set. This is the counterpart to match_on. It supports advanced expression filters that can compare properties between the origin_resource and a candidate target_resource from the with set. |
Note:
create_relationis only valid inline inside[[link_resources]]. A top-level[[create_relation]]block is not a directive and is silently ignored.
[[link_resources]] Patterns
The directive supports distinct patterns depending on which keys are used.
1. Precise Property-Based Joins (join)
This is the most common pattern, used for linking resources based on a shared identifier (like a foreign key).
Goal: For each server, find its corresponding application by matching server.app_id to application.id, copy the application’s owner property, and create a RUNS relationship.
origin_resource = "server"
[[link_resources]]
with = "application"
join = { local = "app_id", remote = "id" }
copy_properties = [
{ from = "owner", as = "server_owner" },
{ from = "version", as = "major_version", template = "{{ value | split(pat='.') | first }}" }
]
create_relation = { type = "RUNS" }
- Graph Impact: A
serverwithapp_id: "app-123"will be joined with anapplicationthat hasid: "app-123". The server will gain aserver_ownerproperty and amajor_versionproperty (e.g.,"2"if the application’s version was"2.7.1"). A new(server) -[RUNS]-> (application)relationship will also be created.
Authoring in Jsonnet (models/*.jsonnet)
In Jsonnet, linking rules use rescile.linkResources(...):
local rescile = import 'rescile/v1/rescile.libsonnet';
rescile.linkResources(
origin='server',
withResource='application',
joinLocal='app_id',
joinRemote='id',
relationType='RUNS',
copyProperties=[
{ from: 'owner', as: 'server_owner' },
{ from: 'version', as: 'major_version', template: "{{ value | split(pat='.') | first }}" }
]
)
Jsonnet Advantage — Multi-Relation Comprehensions: Link an origin to multiple dependency types dynamically without repeating [[link_resources]] tables:
[
rescile.linkResources(
origin='server',
withResource=dep.resource,
joinLocal=dep.key,
joinRemote='id',
relationType=dep.relation
)
for dep in [
{ resource: 'database', key: 'db_id', relation: 'CONNECTS_TO' },
{ resource: 'cache', key: 'cache_id', relation: 'USES_CACHE' },
{ resource: 'network', key: 'net_id', relation: 'IN_NETWORK' },
]
]
2. Expression-Based Joins
For complex join conditions that go beyond simple key equality, you can use an expression in the match_with block. This provides access to both the origin_resource (the resource being processed in the main iteration) and a special target_resource variable.
The target_resource variable refers to a candidate resource from the with set that is currently being evaluated as a potential match. For each origin_resource, rescile iterates through all resources of the with type, evaluating the expression for each (origin_resource, target_resource) pair. Every pair that causes the expression to render exactly "true" is considered a match.
Goal: For each vm_request (origin_resource), find a physical_host (with resource) that has sufficient available_ram. The target_resource variable refers to the physical_host being checked.
origin_resource = "vm_request"
[[link_resources]]
with = "physical_host"
match_with = [
{ expression = "{% if origin_resource.requested_ram <= target_resource.available_ram %}true{% endif %}" }
]
copy_properties = [ { from = "name", as = "assigned_host" } ]
create_relation = { type = "HOSTED_ON" }
- Graph Impact: For each
vm_request,rescilewill iterate through allphysical_hostresources. Theexpressioncomparesvm_request.requested_ramwithphysical_host.available_ram. Every host that satisfies the condition will be linked. Whencopy_propertiesis used and multiple targets match, the copied values are gathered into the origin resource as arrays.
3. Filtered Set Linking (N:M Joins)
This pattern links groups of resources based on filters, not on shared property values. It creates a relationship from every resource in the filtered source set to every resource in the filtered destination set.
Goal: Link all production applications to the central production_gateway.
origin_resource = "application"
[[link_resources]]
match_on = [ { property = "environment", value = "production" } ]
with = "gateway"
match_with = [ { property = "name", value = "production_gateway" } ]
create_relation = { type = "ROUTES_THROUGH" }
- Graph Impact: Every
applicationresource withenvironment: "production"will get a newROUTES_THROUGHrelationship pointing to theproduction_gateway.
4. Unconditional Cross-Join
By omitting on, join, match_on, and match_with, you link every origin_resource to every resource of the with type. This produces a cross-join (N:M) unless there is exactly one with resource in the graph.
Goal: Link every server resource to the single, central subscription resource.
origin_resource = "server"
[[link_resources]]
with = "subscription"
create_relation = { type = "PART_OF" }
- Graph Impact: When only one
subscriptionexists, everyservergets aPART_OFrelationship pointing to it. If multiplesubscriptionresources exist, everyserveris linked to every one of them. To obtain true singleton behavior, add amatch_withfilter that selects a single target.
Comparison of Linking Patterns
| Feature | on / join |
Expression-Based Join | Filtered Set Linking (match_on / match_with) |
Unconditional (Cross-Join) |
|---|---|---|---|---|
| Join Logic | Value Equality: local.prop == remote.prop |
Arbitrary Expression: f(origin, target) -> bool |
Set Intersection: Links filtered sets | Unconditional: Links all sources to every matching remote |
| Cardinality | Typically 1:1 or 1:N | 1:N or N:M (all matches kept) | Can be 1:1, 1:N, N:1, or N:M | N:M unless only one target exists |
| Primary Use Case | Foreign-key style relationships based on shared identifiers. | Resource allocation or complex policy-based joins. | Applying broad, policy-based connections between groups of resources. | Connecting all resources to a central entity; use match_with to enforce a single target. |
Cardinality Enforcement (cardinality = "one")
By default, relationships and property copies assume cardinality = "many". If multiple remote resources match an origin, properties from all targets are copied and multiple edges are created.
When a relationship is semantically single-valued (e.g. an application having exactly one active fee schedule or server), declare cardinality = "one":
origin_resource = "context"
[[link_resources]]
with = "fee_rule"
join = { local = "name", remote = "context" }
cardinality = "one"
copy_properties = [ { from = "rate", as = "active_rate" } ]
create_relation = { type = "HAS_ACTIVE_RULE" }
In Jsonnet:
rescile.linkResources(
origin='context',
withResource='fee_rule',
joinLocal='name',
joinRemote='context',
cardinality='one',
relationType='HAS_ACTIVE_RULE',
copyProperties=[{ from: 'rate', as: 'active_rate' }]
)
Cardinality Behavior:
- Central Schema Integration: If the rule’s created relation is defined in a bound central schema with
"cardinality": "one", Rescile infers single cardinality automatically even if omitted in the rule. - Violation Warning: If more than one target matches during build time, Rescile logs a warning:
WARN: cardinality 'one' violation: rule matched 2 targets for origin 'context:ctx-1' ... - Join-aware ambiguity check: With a
joinclause, only targets that pass the join are counted againstcardinality = "one"— a rule withjoin = { local = "parent", remote = "name" }over a type with many rows still copies and links when exactly one row matches. Without ajoin(cross-join), every candidate of thewithtype counts. - Scalar Protection: When a
"one"cardinality violation occurs, the property copy and edge creation for that origin node are skipped to protect downstream consumers from unexpected array values or duplicate relations.
Copy direction (direction)
By default, the direction of a copy_properties entry is resolved by presence: if the
origin_resource carries the from property, the value is copied origin → target;
otherwise it is copied target → origin. This fallback is intentional (it makes
“inherit from the linked resource” rules work without extra configuration), but it has a
trap: when both sides carry the property, the copy silently inverts to origin → target.
The canonical case is copying an issuer’s not_after onto a subordinate CA — the subordinate
always has a not_after of its own, so the default copies the child’s value onto the
parent instead.
When both sides carry the property — or whenever the direction must not depend on data — declare it explicitly on the copy entry (rename syntax only; simple string entries always use the presence-based default):
[[link_resources]]
with = "ca"
join = { local = "parent", remote = "name" }
cardinality = "one"
copy_properties = [
{ from = "not_after", as = "issuer_not_after", direction = "to_origin" }
]
create_relation = { type = "SIGNED_BY" }
| Value | Meaning |
|---|---|
"to_origin" |
Copy from the join-matched with node onto the origin_resource. |
"to_target" |
Copy from the origin_resource onto the join-matched with node. |
| omitted | Presence-based resolution (the historical default, unchanged). |
Notes:
- With an explicit
direction, the copy keeps its side even when the other side also carries the property, and the change-check applies in both directions. - Multiple matched targets copying onto the same origin (or multiple origins onto the same
target) aggregate into an array — that is the mutation merge semantics, not a direction
effect. Use
cardinality = "one"when a scalar is expected. - An explicit
directionis unavailable in the simple string form ("prop"); use the{ from = "...", as = "..." }form.
Idempotency and Parallel Edges
[[link_resources]] is fully idempotent across model execution passes (the fixpoint loop). The graph store strictly deduplicates identical (source, target, label) relationships. If a [[link_resources]] rule matches the same resources on iteration 2 as it did on iteration 1, it will not pile up duplicate physical edges in the graph backend.
Handling parallel edges with different labels:
If a [[link_resources]] rule and an auto-link property (or another rule) create multiple edges between the same two resources but with different labels (e.g., a BELONGS_TO edge and a resident edge), they are stored as distinct physical edges.
When querying the GraphQL API:
- The fields are cleanly separated by label. Querying the
BELONGS_TOfield will only return targets connected via theBELONGS_TOedge. It will not duplicate the target just because a parallelresidentedge exists. - Property types are isolated. The schema strictly isolates edge property definitions by their specific relationship label. Properties belonging to a
BELONGS_TOedge are never conflated with properties from a parallelresidentedge between the same endpoints.
The only scenario where duplicate nodes will surface inside a single GraphQL field is if you intentionally or accidentally create multiple parallel edges with the exact same string label between the same two nodes.
[[copy_property]] vs. [[link_resources]]: Push vs. Pull
While both directives can add properties to a resource, they serve fundamentally different purposes based on their “push” vs. “pull” nature.
| Feature | [[copy_property]] |
[[link_resources]] (with copy_properties) |
|---|---|---|
| Operation | Push: Pushes data from the origin_resource to a connected node. |
Pull / lookup: The engine tries to copy a property from the origin_resource to the with resource first; only if the origin does not have the property does it fall back to copying from the with resource to the origin. |
| Requirement | An existing, direct connection must be present. | No existing connection required. It finds the remote node via a join condition or filter. |
| Analogy | Property Assignment / Inheritance: connected_object.property = self.property |
Database JOIN / Lookup: tries self.property = remote.property; if self already owns the property, it may be pushed instead. |
| Use Case | Propagating context along an existing relationship graph (e.g., an application pushing its environment to its server). |
Enriching a resource with data from a central “lookup” source (e.g., a server looking up its location from a datacenter resource). |