Relationships
How to create, query, and manage relationships between entities.
Overview
Relationships connect entities to each other. They are stored as part of the entity manifest and versioned along with everything else.
A relationship has:
- predicate -- The type of connection (e.g.,
contains,parent,collection) - peer -- The target entity ID
- peer_type -- Optional type hint for the target
- peer_label -- Optional display name for the target
- properties -- Optional metadata on the relationship itself
{
"predicate": "contains",
"peer": "01KFNR81RMVAX2BBMMBW51V97D",
"peer_type": "chapter",
"peer_label": "Chapter 1: Loomings",
"properties": {
"order": 1,
"added_at": "2025-01-15T10:00:00Z"
}
}Managing Relationships
There are two ways to manage relationships:
1. Via Entity Update (Recommended)
Use PUT /entities/:id with relationships_add or relationships_remove. This is the recommended approach for most use cases -- it's simpler, requires only one CAS guard, and lets you update properties at the same time.
PUT /entities/01KFNR849AZNBWE9DYJRZR7PSA
{
"expect_tip": "bafyreig...",
"relationships_add": [
{
"predicate": "references",
"peer": "01KFNR81RMVAX2BBMMBW51V97D",
"peer_label": "Moby Dick; Or, The Whale",
"properties": { "citation_type": "primary" }
}
],
"relationships_remove": [
{ "predicate": "draft", "peer": "01KFNR81RMVAX2BBMMBW51V97D" }
]
}Upsert semantics: If a relationship with the same predicate and peer already exists, relationships_add merges the properties rather than creating a duplicate.
Removing all relationships with a predicate: Omit the peer field in relationships_remove to remove all relationships with that predicate:
{
"relationships_remove": [
{ "predicate": "draft" }
]
}2. Via Dedicated Endpoint (Bidirectional Only)
Use POST /relationships or DELETE /relationships when you need to atomically update two entities at once (bidirectional relationships).
POST /relationships
{
"source_id": "01KFNR849AZNBWE9DYJRZR7PSA",
"target_id": "01KFNR81RMVAX2BBMMBW51V97D",
"source_predicate": "contains",
"target_predicate": "parent",
"expect_source_tip": "bafyreig...",
"expect_target_tip": "bafyreih..."
}This creates two relationships atomically:
source_predicateon the source pointing to the targettarget_predicateon the target pointing back to the source
Permission requirements:
- Unidirectional: requires
entity:updateon the source - Bidirectional: requires
entity:updateon both entities (unless the target has no collection -- "open season")
Creating Relationships at Entity Creation
Include relationships in the POST /entities request body:
POST /entities
{
"type": "chapter",
"properties": { "label": "Chapter 1. Loomings" },
"collection": "01KFNR0H0Q791Y1SMZWEQ09FGV",
"relationships": [
{ "predicate": "parent", "peer": "01KFNR81RMVAX2BBMMBW51V97D" }
]
}The collection field is a convenience shorthand that creates a relationship with predicate: "collection" and peer_type: "collection".
Common Predicates
| Predicate | Direction | Meaning |
|---|---|---|
contains | Parent to Child | Parent contains the child entity |
parent | Child to Parent | Child belongs to this parent |
collection | Entity to Collection | Entity belongs to this collection |
references | Any | General reference link |
admin | Collection to User | User has admin role in collection |
member | Collection to User | User has member role in collection |
Predicates must be lowercase alphanumeric with underscores (regex: ^[a-z][a-z0-9_]*$).
Querying Relationships
Getting an Entity's Relationships
When you fetch an entity via GET /entities/:id, relationships are included in the response. By default, you get the stored peer_label and peer_type (which may be stale if the target entity was updated).
For fresh peer data, use the expand query parameter:
GET /entities/01KFNR81RMVAX2BBMMBW51V97D?expand=relationships:previewExpansion modes:
| Mode | Description |
|---|---|
| (none) | Returns stored peer_label/peer_type only |
?expand=relationships:preview | Adds peer_preview with fresh lightweight data |
?expand=relationships:full | Adds peer_entity with complete manifest (use with caution) |
Example with preview expansion:
{
"predicate": "contains",
"peer": "01KDOC...",
"peer_label": "Old Label",
"peer_preview": {
"id": "01KDOC...",
"type": "document",
"label": "Updated Label",
"description_preview": "This is a document with...",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-20T14:30:00Z"
}
}Getting Both Incoming and Outgoing Relationships
Entity manifests only store outgoing relationships (what this entity links to). To see incoming relationships (what links to this entity), use the graph endpoint:
GET /graph/entity/01KFNR81RMVAX2BBMMBW51V97DThis returns relationships in both directions from the graph database.
Graph Traversal
For path-based traversal, use the Argo query DSL:
POST /query
{
"query": "@01KFNR81RMVAX2BBMMBW51V97D -[contains]{,3}-> type:chapter"
}This starts at the specified entity and traverses up to 3 hops of contains relationships to find all chapters.
See Argo Queries for the full query syntax.
Finding Reachable Entities
For exhaustive (non-ranked) graph exploration:
POST /graph/reachable
{
"source_pis": ["01KFNR81RMVAX2BBMMBW51V97D"],
"target_type": "chapter",
"max_depth": 4,
"direction": "outgoing"
}Finding Paths Between Entities
To find how two entities connect:
POST /graph/paths
{
"source_pis": ["01KFNR81RMVAX2BBMMBW51V97D"],
"target_pis": ["01KCHAPTER123..."],
"max_depth": 4
}Relationship Properties
Relationships can have arbitrary metadata in the properties field:
{
"predicate": "admin",
"peer": "01JUSER...",
"peer_type": "user",
"properties": {
"granted_at": "2025-01-15T10:00:00Z",
"granted_by": "01JOTHERUSER...",
"expires_at": "2025-12-31T00:00:00Z"
}
}When updating a relationship that already exists (same predicate + peer), the properties are deep merged with the existing properties. To remove specific properties, use properties_remove in the relationship add:
{
"relationships_add": [
{
"predicate": "admin",
"peer": "01JUSER...",
"properties_remove": ["expires_at"]
}
]
}Bidirectional Relationship Handling
For relationships that should be symmetric (like contains/parent pairs), you have two options:
Option 1: Manual management -- Add both relationships yourself via two separate updates or one update with multiple relationships_add entries.
Option 2: Atomic bidirectional -- Use POST /relationships with both source_predicate and target_predicate to update both entities atomically with CAS guards on both.
The atomic approach is safer for consistency but requires entity:update permission on both entities.