Skip to content

13.4 Derived relationships — an edge whose existence is a conclusion

A head may be a relationship pattern; the rule set then defines a derived relationship type, named by derives: exactly as a label would be:

rules/shared-failures.yml
name: gov-uk/shared-failures
derives: SHARES_FAILURES
description: >-
Two officers are related when the register shows both appointed at two or more of the same
now-dissolved companies; `failures` counts the companies they share.
rules:
- derive: "(a)-[:SHARES_FAILURES {failures: n}]->(b)"
from: |
MATCH (a:Officer)-[:APPOINTED_AT]->(c:Company {status: 'dissolved'})<-[:APPOINTED_AT]-(b:Officer)
WHERE a.officerId < b.officerId
WITH a, b, count(DISTINCT c.companyNumber) AS n
WHERE n >= 2

The head’s shape is validated the way a node head’s is, and each restriction is a leg of the same guarantee:

  • Exactly one relationship, directed. (a)<-[:T]-(b) is accepted and normalised by swapping the endpoints; an undirected head is refused — a derived edge has to be written in one direction.
  • Endpoints are two bare, DISTINCT variables the body binds. A label on an endpoint is refused: the body binds the variables, so a label there constrains nothing. A head naming the same variable twice is refused.
  • Membership is the ordered pair, and a later round that re-concludes a pair UPDATES its properties rather than laying a parallel edge — the second clause of a recursive set settles values exactly as a derived label’s properties settle.
  • One set concludes one kind. Label rules and relationship rules are different fixpoints; mixing them in one set is refused (they remain free to reference each other across sets — a label rule may match a derived relationship, and vice versa).

Once concluded, the edge is ordinary: a later rule recurses over it, a query traverses it, and the schema declares the relationship — with the endpoint kinds its bodies bind — before a single pair exists, exactly as a derived label is declared before a single member is.

maxPairs is the budget that node sets never needed. Node membership is bounded by the node count, and the fragment’s own argument terminates it. Pair membership is bounded by the square of the node count, so an under-constrained body is quadratic in time and in transient writes long before maxRounds would notice. Exceeding maxPairs (default 250 000) fails loudly rather than returning a truncated relation — a cross join is the first mistake an edge rule’s author makes, and a loud refusal is the correction; a silent truncation would be a wrong answer wearing a right one’s shape. The setting is ignored by a set that derives a label.

A derived relationship is not transitive closure. A path already in the graph needs no rule — *1..n traverses it. Derive an edge when its existence is a conclusion no traversal states: two officers related because they share two or more failed companies; a supplier EXPOSED_TO a sanctioned party once the aggregated stake crosses a threshold. If the rule’s body is nothing but a path over existing edges with no aggregation, threshold, or judgment, write the path in the query instead.