Arke
Build

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:

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_predicate on the source pointing to the target
  • target_predicate on the target pointing back to the source

Permission requirements:

  • Unidirectional: requires entity:update on the source
  • Bidirectional: requires entity:update on 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

PredicateDirectionMeaning
containsParent to ChildParent contains the child entity
parentChild to ParentChild belongs to this parent
collectionEntity to CollectionEntity belongs to this collection
referencesAnyGeneral reference link
adminCollection to UserUser has admin role in collection
memberCollection to UserUser 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:preview

Expansion modes:

ModeDescription
(none)Returns stored peer_label/peer_type only
?expand=relationships:previewAdds peer_preview with fresh lightweight data
?expand=relationships:fullAdds 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/01KFNR81RMVAX2BBMMBW51V97D

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

On this page