---
title: "Facts that follow: symmetric, inverse and transitive relationships, each answer with its proof"
slug: knowledge-layer-5-reasoning
date: 2026-10-21
author: Semurg
section: how-to
tags: [knowledge-graph, reasoning, neurosymbolic, ontology, python, how-to]
summary: Store "Ada is married to Charles" and recall Charles, and the memory answers that Charles is married to Ada, marked as derived, with its rule and proof. The same for inverse and transitive relationships.
status: published
covers: [sdk:remember, sdk:recall]
constants: ["0.1.1", 6]
---

Some facts follow from others by what a relationship means. If Ada is married to Charles, Charles is married to Ada. If Byron is the
parent of Ada, Ada is the child of Byron. If the Graph Team is part of Research and Research is part of Acme, the Graph Team is part of
Acme. You should not have to store each of those twice, and when the memory answers with one, it should show its work.

> Diagram three-rules: Three rules. Symmetric: a married_to b gives b married_to a. Inverse: a parent_of b gives b child_of a. Transitive: a part_of b and b part_of c give a part_of c. Each derived answer names its rule and the stored facts it rests on.

## Store four facts, read the ones that follow

The program below ran as written against a fresh node of release r11.121.20261021.3619d35, with semurg 0.1.1 from PyPI. Save it as `reasoning.py` (with a node and `SEMURG_API_TOKEN` set, as in [the PyPI quickstart](/blog/pip-install-semurg)):

```python
import os
from semurg import Client

with Client("http://127.0.0.1:4000", token=os.environ["SEMURG_API_TOKEN"]) as mem:
    mem.remember("org", facts={
        "entities": [{"name": "Ada", "type": "person"}, {"name": "Charles", "type": "person"},
                     {"name": "Byron", "type": "person"}, {"name": "Graph Team", "type": "team"},
                     {"name": "Research", "type": "department"}, {"name": "Acme", "type": "company"}],
        "relations": [{"source": "Ada", "target": "Charles", "type": "married_to"},
                      {"source": "Byron", "target": "Ada", "type": "parent_of"},
                      {"source": "Graph Team", "target": "Research", "type": "part_of"},
                      {"source": "Research", "target": "Acme", "type": "part_of"}],
    })

    for who in ["Charles", "Ada", "Graph Team"]:
        for item in mem.recall("org", subject=who)["items"]:
            if item["kind"] == "fact" and item.get("derived"):
                proof = " + ".join(f'{d["subject"]} {d["relation"]} {d["object"]}' for d in item["derivation"])
                print(f'{who} {item["relation"]} {item["text"]}  ({item["rule"]}: {proof})')
```

It prints the facts that were never stored, each with the rule that produced it and the stored facts it rests on:

```text
Charles married_to Ada  (symmetric: Ada married_to Charles)
Ada child_of Byron  (inverse: Byron parent_of Ada)
Graph Team part_of Acme  (transitive: Graph Team part_of Research + Research part_of Acme)
```

## How the answers are marked

A derived fact comes back in the same list as stored ones, so an agent reads one answer, but it is never passed off as stored. It has
`derived: true`, its `rule` (`symmetric`, `inverse` or `transitive`), and a `derivation`: the stored facts it follows from, each one
recallable on its own. Its citation says how it was found (`found_by.reasoning`), where a stored fact cites the source it came from.
That makes every derived answer checkable: follow the derivation back to the stored facts and their sources.

## Which relationships carry a meaning

The rules come with the relationship's name. This is the whole list in the current release:

| Rule | Relationships | What follows |
|---|---|---|
| symmetric | `married_to`, `spouse_of`, `sibling_of`, `partner_of`, `neighbor_of` (or `neighbour_of`), `adjacent_to`, `borders`, `colleague_of`, `twin_of` | b relates to a when a relates to b |
| inverse | `parent_of` and `child_of`, `owns` and `owned_by`, `employs` and `employed_by`, `manages` and `managed_by`, `acquired` and `acquired_by`, `founded` and `founded_by`, `has_part` (or `contains`) and `part_of`, `has_subsidiary` and `subsidiary_of`, `teaches` and `taught_by`, `supervises` and `supervised_by`, `leads` and `led_by`, `authored` and `authored_by`, `created` and `created_by` | each side of the pair from the other |
| transitive | `part_of`, `located_in`, `contained_in`, `subsidiary_of`, `ancestor_of`, `descendant_of`, `subclass_of`, `within` | a chain of up to 6 links collapses to one answer |

Names are matched in any letter case and with spaces or hyphens in place of underscores: `MARRIED_TO`, `Part of` and `part-of` all
match. So in the example above, recalling Research also answers that Research has part the Graph Team, by the inverse of `part_of`. The
list is fixed: the rules do not come from the ontology, and you cannot add your own today. A relationship outside it is stored and
returned exactly as sent, with nothing derived from it. The recall tool's description over MCP names the most common of these rules, so an
agent learns them from the tool itself.

## Why it matters

An agent asked "who is Charles married to?" gets an answer even though only Ada's side was stored, and the answer carries its proof.
That is the difference between a memory that matches strings and one that knows what its relationships mean, and it is why the types and
relation names your agents choose matter. The classes and relation types a scope has used are listed by `memory_status`; see
[the automated ontology](/blog/knowledge-layer-2-the-automated-ontology).

Earlier in this series: [Graph RAG that says when it does not know](/blog/knowledge-layer-4-graph-rag) and
[memory with a timeline](/blog/knowledge-layer-3-time-and-history). Next: [scopes, separate memories shared on purpose](/blog/knowledge-layer-6-scopes).
