Data Relations
A relation connects a row in one of your collections to a row in another —
an icon that belongs to a technology, a certification earned for a
tech, a reference tied to an experience. This page covers the full
round trip: declaring the relation in your schema, seeding the actual links
from your repo, and reading them back.
If you haven't pushed a data model yet, start with Custom Database Collections — this page builds on it.
The one thing to internalize
A relation is never a column. The value doesn't live inside the row's fields — it lives in a dedicated link table, one entry per connection, keyed by the two rows it joins.
That has a practical consequence for seeding: putting a relation key inside
values does nothing. The validator strips unknown and relation keys from
values by design, so a seed like
values: { svg: "…", tech: "some-id" } writes the svg and silently
drops tech. Links have their own channel: the relations block, below.
Why it works this way: links reference rows by id, ids don't exist until rows do, and a link's target may be seeded by a different file than its source. Keeping links out of the row lets the platform write every record first and resolve every link after — so ordering across your seed files never matters.
1 · Declare the relation in your schema
A RELATION field lives on the object that points — the icon points at its
technology, so the field goes on icon:
defineField({
universalIdentifier: '3d9e...',
name: 'tech',
label: 'Technology',
type: 'RELATION',
relationTarget: 'a8da...', // the technology OBJECT's universalIdentifier
onDelete: 'restrict', // restrict (default) | setNull | cascade
})
relationTargetis the object'suniversalIdentifier— the collection it points at, not any particular row.onDeletedeclares what happens when a target row is deleted while something still links to it:restrictrefuses the delete (default),setNulldrops the links and keeps both rows,cascadesoft-deletes the rows that pointed at it.
Push and promote as usual (sync-data-model, then promote in the dashboard).
The field now exists on the model — but it holds no data until you link rows.
2 · Seed the links
Rows in ./biab-records gain an optional relations block, keyed by the
relation field name, valued by the target row's universalIdentifier:
// biab-records/icons.json
[
{
"object": "81bb...", // the icon OBJECT's uid
"universalIdentifier": "fda0...", // this icon ROW's uid
"values": { "svg": "<svg …/>", "version": 1 },
"relations": {
"tech": "d1c4...", // one target row
"tags": ["9e2a...", "77b0..."] // or several
}
}
]
Then npx biab-dev sync-records as always. The CLI syncs in two passes —
all records first, then all relations — so a link can reference a row seeded
by any file, in any order, including rows from previous syncs.
Relations converge, like everything else in a seed
Re-running a seed doesn't duplicate rows, and it doesn't duplicate links
either. For each field you name in relations:
- links you declare that don't exist are added;
- links that exist but aren't declared are removed;
"tags": []explicitly clears the field's links.
Fields you don't name are left completely alone — so links added by hand in the dashboard survive a re-seed, as long as your seed doesn't claim to manage that field.
Errors are loud, not lossy
A typo'd field name, or a target universalIdentifier that doesn't match any
row in the target collection, fails that row's relations with a specific
error — the record itself still saves, the rest of the batch still runs, and
the CLI exits non-zero so CI notices. Nothing is guessed and nothing is
silently skipped.
3 · Read them back
The records read returns relations resolved, keyed by field name — every
declared relation field is always present, [] when it has no links:
{
"id": "…",
"universalIdentifier": "fda0...",
"fields": { "svg": "<svg …/>", "version": 1 },
"relations": {
"tech": [{ "recordId": "…", "object": "a8da..." }],
"tags": []
}
}
Each link gives you the target row's id plus the universalIdentifier of the
object it belongs to; fetch the target with a follow-up read when you need its
fields.
Rules and limits
- Relation keys inside
valuesare stripped. Therelationsblock is the only channel that writes links. - Targets are row
universalIdentifiers — the same identity that makes record seeds converge. Rows created by hand in the dashboard have nouniversalIdentifierand can't be targeted from a seed. MORPH_RELATIONcan't be seeded yet. Declare links for single-targetRELATIONfields; polymorphic links are dashboard-managed for now.- Custom objects only — same rule as record seeding. Standard objects (contacts, products, …) have their own APIs with their own business rules.
- Duplicate declarations of the same link are harmless; the link table is unique per (field, source, target).