Architectural Models

Linking Resources

How to use [[link_resources]] to join resources by key, expression, or filter and pull data from them.

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.

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_relation is only valid inline inside [[link_resources]]. A top-level [[create_relation]] block is not a directive and is silently ignored.

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 server with app_id: "app-123" will be joined with an application that has id: "app-123". The server will gain a server_owner property and a major_version property (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, rescile will iterate through all physical_host resources. The expression compares vm_request.requested_ram with physical_host.available_ram. Every host that satisfies the condition will be linked. When copy_properties is 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 application resource with environment: "production" will get a new ROUTES_THROUGH relationship pointing to the production_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 subscription exists, every server gets a PART_OF relationship pointing to it. If multiple subscription resources exist, every server is linked to every one of them. To obtain true singleton behavior, add a match_with filter 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 join clause, only targets that pass the join are counted against cardinality = "one" — a rule with join = { local = "parent", remote = "name" } over a type with many rows still copies and links when exactly one row matches. Without a join (cross-join), every candidate of the with type 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 direction is 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_TO field will only return targets connected via the BELONGS_TO edge. It will not duplicate the target just because a parallel resident edge exists.
  • Property types are isolated. The schema strictly isolates edge property definitions by their specific relationship label. Properties belonging to a BELONGS_TO edge are never conflated with properties from a parallel resident edge 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.

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).