> For the complete documentation index, see [llms.txt](https://flapjax.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://flapjax.gitbook.io/docs/core-concepts/relationships.md).

# Relationships

**Audience:** advanced platform builders and integrators. This page covers how the core data entities link together, how reference attributes are stored and resolved, and the limits on what a filter or action can traverse. That last point matters most.

It is the conceptual map behind reference normalisation, automatic data creation, the Related Records modal and the stack attribute form. For plain-language definitions, see the [glossary](/docs/start-here/key-concepts.md).

***

## 1. The entities

Flapjax has four data-bearing entity kinds, all scoped to your organization.

| Entity     | Identity                                        | Custom fields                                       | Notes                                                           |
| ---------- | ----------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------- |
| **Person** | Platform UUID, plus your optional `external_id` | Attribute values, defined by your people attributes | Deleting is a soft delete, history is retained                  |
| **Record** | Platform UUID, plus your optional `external_id` | Attribute values, defined by the stack's attributes | Belongs to exactly one stack. Soft-deleted                      |
| **Stack**  | Key (machine name)                              | Its schema is its stack attributes                  | A customer-defined "table", typed `events`, `items` or `signal` |
| **List**   | Platform UUID                                   | List attributes on each item                        | A curated set of records **or** people                          |

A **record** always belongs to exactly one stack, and the **stack's type** governs what clients may do with its records. `events` stacks are create-only and frozen. `items` stacks take the full set of create, read, update and delete. `signal` stacks are written by automations alone and read-only to clients.

### Identity conventions

* **Platform IDs and your own.** Every person and record carries a platform-issued UUID. You can also address it by *your own* `external_id`, supplied at creation.
* **Structural ownership is fixed.** A record's membership in its stack, a list item's membership in its list and a person's label memberships are hard links the platform maintains. Deleting the parent cleans them up.
* **Reference&#x20;*****attributes***, covering person to record, record to person and record to record, are ordinary attributes whose value is a link. They live inside the entity's attribute values, as §2 covers. That is what makes them flexible and auto-createable, and what limits traversal, as §5 covers.

### The entity-relationship map

```mermaid
erDiagram
    STACK ||--o{ RECORD : "contains"
    STACK ||--o{ STACK_ATTRIBUTE : "defines schema"
    PERSON ||--o{ PEOPLE_ATTRIBUTE : "defines schema (org-wide)"
    RECORD }o--o{ PERSON : "person / people reference attribute"
    RECORD }o--o{ RECORD : "stack reference attribute"
    PERSON }o--o{ RECORD : "person's stack reference attribute"
    PERSON }o--o{ LABEL : "label membership"
    LIST ||--o{ LIST_ITEM : "contains"
    LIST_ITEM }o--|| RECORD : "a record (or...)"
    LIST_ITEM }o--|| PERSON : "a person"
```

Two relationship styles coexist:

* **Membership relationships** are true many-to-many links the platform maintains: Person to Label, and List to member, where each list item is exactly one record or one person.
* **Attribute relationships**, meaning references, have a person or record point at another entity by storing its identifier in an attribute value. Covered next.

***

## 2. Reference attributes

An attribute becomes a *reference* through its **data type**. Three reference data types exist, under these catalogue display names.

| `data_type` key | Display name          | Points at                      | Cardinality                     |
| --------------- | --------------------- | ------------------------------ | ------------------------------- |
| `person`        | Related Person (One)  | a single **person**            | one                             |
| `people`        | Related People (Many) | multiple **people**            | many                            |
| `stack`         | Related Stack         | **record(s)** in another stack | `one` **or** `many`, per config |

Which reference types are allowed depends on the attribute's **scope**:

* **Stack attributes** take `person`, `people` or `stack`, so a record references people and other records.
* **Person attributes** take `stack` alone. A person references records, say a person's orders, and a person attribute can never be a `person` or `people` reference.

### `relation_type` and `relation_stack_id`

For a `stack`-typed attribute (record reference), the target and cardinality live in the attribute's configuration:

```json
{ "relation_type": "one", "relation_stack_id": 42 }
```

| Field               | Meaning                                                                                                   |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| `relation_type`     | `"one"` for a single linked record, stored as a single value, or `"many"` for several, stored as an array |
| `relation_stack_id` | The ID of the target stack the reference points into                                                      |

> The **data type itself** expresses the difference between `person` and `people`: `person` means one, `people` means many. `relation_type` applies to the `stack` record-reference data type alone. In the rule builder, the single-or-many hint shapes the value picker and the validation.

### How a reference is stored

A written reference is stored as an **identifier object** rather than a raw scalar, so the link survives before anyone enriches the target.

| Attribute type | Stored shape (single)                                       | Stored shape (many)                                   |
| -------------- | ----------------------------------------------------------- | ----------------------------------------------------- |
| `person`       | `{ "internal_id": 123 }` or `{ "external_id": "user-001" }` | ,                                                     |
| `people`       | ,                                                           | `[ { "internal_id": 123 }, { "external_id": "u2" } ]` |
| `stack`        | `{ "record_id": "uuid" }` or `{ "external_id": "ord-1" }`   | `[ { "record_id": "uuid" }, … ]`                      |

On write, plain strings and numbers get **normalised** into these identifier objects, and trimmed. See reference normalisation for the exact rules. Person attributes convert strings and numbers. Stack attributes convert strings alone. Empty and whitespace values clear the link.

A reference resolving *inside an automation*, as an operand, materialises to the same identifier-object shape. Person values become `{internal_id}` or `{external_id}`. Record and stack values become `{record_id}` or `{external_id}`. See [Actions → Variables & Fallback Values](/docs/automations/actions.md) for how reference variables work in automation fields.

***

## 3. Reverse references

The stored identifier lets you follow a reference **forwards**, since a record tells you which person it links to. Following it **backwards**, asking "which records point *at* this person?", takes a dedicated reverse lookup, which the platform performs on demand.

The reverse lookup checks every person-reference attribute across your organization's stacks, leaving out `events` stacks, whose records never change. It returns the matching records grouped by stack. An equivalent lookup covers record-to-record references.

**Where this surfaces.** Reverse lookups back the **Related Records** experience: the person-detail "associated records", the record-detail "related records" cards, and the Related Records modal's "View all" pagination. They also identify affected records before a person is deleted, so no delete leaves dangling references.

References live in attribute values rather than a dedicated link table, which makes the reverse direction a broader scan than a forward lookup. That is why it arrives as a dedicated experience rather than a general filter capability.

> Reverse lookup matches on the resolved person. A reference stored purely as `{external_id: "…"}`, meaning an unresolved stub with no real person behind it yet, stays invisible to person-side reverse lookups until it resolves.

***

## 4. Auto-create & normalization on write

References are built so you never pre-create the thing you point at. Write a reference whose target does not exist and the API creates a **minimal stub**, meaning a person or record carrying the given `external_id` alone, then links to it. A later write **enriches** that stub with real data and keeps the link intact.

The behaviour comes in two parts, each with its own guide. This page duplicates neither.

* **Normalisation** wraps plain values into identifier objects, trims them, and clears the link on empty. See reference normalisation.
* **Auto-creation and enrichment** create stubs for missing targets. See automatic data creation.

REST and AMQP ingestion apply the same normalisation and auto-create behaviour, so results match across both transports.

***

## 5. Traversing relationships in filters and actions, and the limits

This is the part people over-claim most, so read it carefully. **Filters match on the reference value itself. They never join through a reference into the target entity's own fields.**

### What a relationship rule actually does

A filter rule targeting a reference attribute compares the stored identifier against the value you supply. "Is this record's `customer` reference equal to this person?"

**No join** reaches into the referenced person or record, which leaves **no way to filter records by a field on their linked entity**. "Orders whose linked customer's `country = DE`" cannot be expressed in a single rule group.

**Labels** are the one relationship filters truly traverse. "Person has label X" and label-category rules evaluate against real label membership. That works because Person to Label is a true membership relationship rather than a reference attribute.

### What you *can* do

* **Filter on a specific link.** "Records where `customer` = this person" is the mechanism behind the Related-Records drill-down, which encodes exactly that rule into a base64 `?filter=` query.
* **Empty and not-empty** on a reference, asking whether this record links anything.
* **Reverse membership** through the dedicated lookups in §3, meaning Related Records, which run outside the rule evaluator.
* **Multi-hop by materialising values into records.** To act on a linked entity's attributes, resolve the reference to a value and store or compare it as data. A workflow can run **Find Records** in one stack, **Loop**, then **Get**, **Aggregate** or **Update** in another, carrying values through the run's context. The join runs procedurally across actions rather than declaratively in one rule.

### Record actions and references

The record-oriented actions each target a single stack: Find Records, Get Record, Aggregate Records, and Create, Update and Delete Record. They filter with the rule semantics described above, so they select records by their own attributes, a reference attribute matched against a target resolved from context included. None of them performs a relational join across stacks. Aggregate Records computes per-stack aggregates over the matched set.

Reporting holds one partial exception outside the rule engine. The dashboard **insight report** engine cross-links people and records through a linking reference attribute as it aggregates. That is bespoke report logic rather than a general filter-traversal capability.
