RelationshipsDefining Relationships

Defining Relationships

A relationship is an edge between two entity instances. Relationships model the connections between entities — who follows whom, which users are friends, which tickets belong to which shows.

The engine manages relationship edges in a dedicated store. You do not need to build a separate join table projection to track simple membership — though you can use projections to query relationship data efficiently.


Syntax

relationships:
  artist_followers:
    from: user
    to:   artist
    cardinality: many_to_many
    unique: true
    emit_events:
      created: ARTIST_FOLLOWED
      removed: ARTIST_UNFOLLOWED
 
  friend_requests:
    from: user
    to:   user
    cardinality: many_to_many
    unique: true
    emit_events:
      created: FRIEND_REQUEST_SENT
      removed: FRIEND_REQUEST_CANCELLED
 
  show_attendees:
    from: user
    to:   show
    cardinality: many_to_many
    unique: true
    emit_events:
      created: CONCERT_ATTENDED

Fields

FieldRequiredDescription
fromYesThe source entity type.
toYesThe target entity type.
cardinalityYesmany_to_many or one_to_many.
uniqueNoIf true, duplicate edges are no-ops. Default: false.
emit_eventsNoEvent types to emit when edges are created or removed.

Cardinality

many_to_many

Any number of from entities can be connected to any number of to entities. A user can follow many artists. An artist can be followed by many users.

one_to_many

One from entity can be connected to many to entities, but each to entity can only have one from entity. Useful for ownership relationships — a user owns many orders, but each order belongs to one user.


unique: true

When unique: true, the engine deduplicates edges at the storage level. If a relationship_create operation produces an edge that already exists (same from_id + to_id), it is treated as a no-op — no error, no duplicate edge.

This is important for idempotency. If a FOLLOW_ARTIST intent is submitted twice (e.g. due to a retry), the second relationship_create is silently ignored at the engine level.

Note: unique: true is a no-op deduplication, not a semantic validation. If you want to surface an error when a user tries to follow the same artist twice, you should still add an explicit preflight rule that rejects the duplicate attempt. unique: true only prevents the data corruption — it does not give the caller a meaningful error code.

# Preflight check pairs naturally with unique: true
actions:
  FOLLOW_ARTIST:
    state: user
    entity_id_expr: intent.user_id
    input:
      user_id:   { type: string, required: true }
      artist_id: { type: string, required: true }
    preflight:
      rules:
        - name: not_already_following
          when: { expr: "LOOKUP_IS_FOLLOWING == true" }
          then:
            - op: reject
              code: ALREADY_FOLLOWING
              message: "Already following this artist"
    core:
      rules:
        - name: create_follow
          when: {}
          then:
            - op: relationship_create
              relationship: artist_followers
              from_id: intent.user_id
              to_id:   intent.artist_id

emit_events

When an edge is created or removed, the engine can automatically emit the configured event types. This decouples the edge management from the event emission — you don’t need a separate op: emit in side_effects.

relationships:
  artist_followers:
    from: user
    to:   artist
    cardinality: many_to_many
    unique: true
    emit_events:
      created: ARTIST_FOLLOWED
      removed: ARTIST_UNFOLLOWED

The emitted event must be declared in events: with a payload that makes sense for the relationship context. The engine populates the event with the relationship metadata (from_id, to_id).

If you need custom payload fields on the emitted event (e.g. followed_at), emit the event explicitly in core or side_effects instead of using emit_events on the relationship.


Operations

relationship_create

Creates an edge between two entity instances. Allowed in core rules.

core:
  rules:
    - name: create_follow
      when: {}
      then:
        - op: relationship_create
          relationship: artist_followers
          from_id: intent.user_id
          to_id:   intent.artist_id

relationship_remove

Removes an edge. Allowed in core rules.

core:
  rules:
    - name: remove_follow
      when: {}
      then:
        - op: relationship_remove
          relationship: artist_followers
          from_id: intent.user_id
          to_id:   intent.artist_id

Both operations are allowed in core — this is the one class of cross-stream operation permitted in core. You are modifying a shared relationship store, but the operation is managed by the engine and does not require loading a second entity’s state.


Querying relationship data

To query which entities are connected via a relationship, project the relationship events into a projection table and query it via a join.

# projections/follow.projections.causet
projections:
  user_following:
    source_events: [ARTIST_FOLLOWED, ARTIST_UNFOLLOWED]
    target:
      table: user_following
      primary_key: [user_id, artist_id]
    fields:
      user_id:     TEXT
      artist_id:   TEXT
      followed_at: BIGINT
    derive:
      user_id:     event.user_id
      artist_id:   event.artist_id
      followed_at: event.ts
    mutations:
      ARTIST_FOLLOWED:   { op: upsert }
      ARTIST_UNFOLLOWED: { op: delete }

Then join in a query:

queries:
  shows_for_followed_artists:
    from: artist_show_directory
    joins:
      user_following:
        on:
          artist_show_directory.artist_id: user_following.artist_id
        fields:
          - user_following.user_id
    fields:
      - artist_show_directory.*
    input:
      user_id: { type: string, required: true }
    where:
      user_following.user_id: { eq: input.user_id }
    order_by:
      date: asc
    limit: 50

Next steps