Relations¶
A relation is a typed edge between two objects on the graph. Like
objects, relations have a type (string), an id (framework-generated),
optional data (dict of JSON-encodable values), and they live in the
event log — created by add_relation, removed by remove_relation,
each transition emitted as an event.
What makes relations distinctive in this framework is that the relation type itself can carry behavior. A relation isn't just a passive edge for the graph projection to render; it can be a rule that fires when its endpoints change, or an agentic actor with its own LLM-backed reasoning. The relation type is the unit of coordination logic between its endpoints.
This is the framework's most differentiated primitive. Most graph frameworks have nodes-with-behavior; relations-with-behavior is where Active Graph diverges.
The three relation kinds¶
Three flavors of relation type, on a spectrum of how much logic the relation itself owns:
- Passive. No behavior attached. The relation is structural
data — it exists, pattern subscriptions can match on it,
behaviors on the endpoints can read it. The vast majority of
relations are passive (
supports,contradicts,cites,depends_on). - Rule. A
@relation_behaviorattached to the type. Fires deterministically when an event affects either endpoint of any relation of that type. Used for coordination logic that semantically belongs to the relationship, not to either endpoint (e.g., adepends_onrelation that auto-blocks the dependent when the dependency changes status). - Agentic. A
@relation_behaviorthat wraps an LLM call (same@llm_behaviormachinery, but anchored on relation events). Used when the coordination logic needs LLM reasoning — e.g., acontradictsrelation that drafts a contradiction-resolution memo when both endpoint claims change.
The three flavors share the same event types
(relation.created, relation.removed) and the same data
representation. The flavor is a property of the relation type,
not of any individual relation instance.
The @relation_behavior decorator¶
from activegraph import relation_behavior
@relation_behavior(
name="auto_unblock",
relation_type="depends_on",
on=["task.completed"],
)
def auto_unblock(relation, event, graph, ctx):
if event.payload["task_id"] == relation.source:
graph.patch_object(relation.target, {"status": "open"})
The body receives the relation (the typed edge instance), the
triggering event, the graph, and the ctx. The relation
behavior fires once per relation that matches — if three
depends_on edges all point at the same source and the source's
task.completed event fires, the body runs three times, once per
edge, each call with that edge as relation.
The decorator's relation_type= argument narrows dispatch to one
type. Other arguments (on=, where=, pattern=) work the same
as on regular @behavior. See behaviors for the
full activation model.
When to use a relation behavior vs a regular behavior¶
The test: does the coordination logic semantically belong to the relationship, not to either endpoint?
- A
depends_onrelation auto-unblocking the dependent when the dependency completes → relation behavior. The unblock logic is about the relationship, not about either task in isolation. - A
claimgetting flagged when itsconfidencedrops below 0.5 → regular behavior onobject.patched. The flag is about the claim itself; no relationship is involved. - A
contradictsrelation drafting a resolution memo when both endpoints change → agentic relation behavior. The reasoning needs both endpoints' state; it's relationship logic, not endpoint logic.
When the test is ambiguous (the logic could go either way), default
to regular behaviors. They're more discoverable — they show up
under the endpoint's type in inspect --behaviors, and the
coordination logic appears as a single behavior fire rather than
N fires (one per matching edge).
Pattern subscriptions and relations¶
Pattern subscriptions match on relations naturally. The Cypher-subset
syntax (a:type1)-[r:rel_type]->(b:type2) binds both endpoints and
optionally the relation itself. See
patterns for the binding rules and when to use
the r variable vs the bare -[:rel_type]-> form.
A behavior with a pattern subscription that mentions a relation type
fires when the pattern matches — which is a different activation
mechanism from @relation_behavior (which subscribes to events on
relation endpoints rather than to graph structure). Both are valid;
pick by which question you're asking: "fire when this edge plus
this surrounding structure exist" (pattern) vs "fire when something
happens to either end of any edge of this type" (relation behavior).
What's related¶
graph— the world state relations sit on. Relations are projections ofrelation.created/relation.removedevents, same as objects.behaviors— the broader behavior model.@relation_behavioris a sibling of@behavior/@llm_behavior.patterns— pattern subscriptions that match on relation structure.- Writing relation behaviors — practical how-to; the decision rules for relation vs regular behavior get more attention there.