Relations
Link rows across any two tables — relation columns, edges, and cardinality.
A relation links rows in one table to rows in another. Unlike other columns, the linked values do not live in the row: each link is an edge stored in the tenant database with provenance (api, cli, tool, workflow, system), timestamps, and archive history. The relation column is the anchor that renders those edges on the grid and record page.
Relations connect any plain table — a CSV import, a TAM build, or another working dataset — and may point at an existing CRM object table. A plain table stays a plain Table: the definition and its edges live in Tables, and creating the relation never registers either endpoint as a CRM custom object. Standard CRM object tables remain template-managed, so they are targets rather than sources for a new native definition.
Link two tables
Start here. One command finds the column that joins your two tables, works out which way the link runs, and shows you how well it actually matches — before anything is written.
# See the plan. Free, writes nothing.
oxygen tables link people --to companiesLinking "people" -> "companies"
Matching on Website -> Domain
(ignores http://, www., and capitalization)
Direction Each people row links to one companies row. Each companies row can have many.
Measured on 5,000 of 12,431 rows
[##############################------] 4,611 will link 92%
49 ambiguous (skipped)
340 with no match yet
Free — 0 credits. Nothing has been written.
Apply: oxygen tables link people --to companies --approved# Apply. Queues a background run; watch it with `oxygen table-runs wait <run_id>`.
oxygen tables link people --to companies --create-missing --approvedYou never name a normalization, a cardinality, a relation slug, or an inverse slug — Oxygen derives all of them from your data and shows the result as a sentence.
| Flag | What it does |
|---|---|
--on <column> | Match on this column instead of the one Oxygen picked. Accepts the column's name or key. |
--create-missing | Also create target Table rows for native links, or CRM Records when filling an existing CRM relationship. Free. |
--approved | Apply. Without it you get the preview. |
--undo <run_id> | Archive exactly the links that run created. Nothing else is touched. |
Things worth knowing:
- Ambiguous rows are never linked. If a row's key matches more than one row on the other side, Oxygen reports it and skips it rather than guessing. Fix the duplicates with
tables dedupeand re-run — re-running only retries the rows that didn't link. - Every number names its sample. "Measured on 5,000 of 12,431 rows" means exactly that. Large tables are measured on a bounded sample, so the counts are estimates and say so.
- If a relation already exists between the two tables, it is filled, not duplicated.
oxygen tables link people --to companiesfills the standardCompanyrelation your CRM already ships. - It's free, and re-running is safe: rows that already linked are left alone.
MCP: oxygen_tables_link_bulk.
Relate two tables by hand
The commands below define a relation explicitly. Reach for them when you want to name the slug and cardinality yourself — otherwise use tables link --to above.
# Preview first (default is dry-run)
oxygen tables relate invoices vendor \
--target-table vendors \
--cardinality many_to_one \
--inverse-slug invoices \
--json
# Apply
oxygen tables relate invoices vendor \
--target-table vendors \
--cardinality many_to_one \
--inverse-slug invoices \
--live --jsonThis creates a vendor relation column on invoices (and an invoices inverse column on vendors). MCP: oxygen_tables_relate.
- The relation slug doubles as the relation column key on the source table (snake_case).
- The target may be any table, including CRM object tables (companies, people, deals).
- Definitions and links are stored in
ox_tables.relation_definitionsandox_tables.relation_edges; the linked values are projected into relation cells at read time. - Definitions are free internal schema writes — no credits. Self-relations (a table to itself) are allowed with distinct forward and inverse slugs.
- Standard CRM object tables cannot be the source (their schemas are template-managed) — point the relation at them and navigate back via the inverse.
Cardinality
--cardinality reads source:target and controls how many active links each row may hold. Writing past a one side archives the previous edge (visible in history, not deleted).
| Value | Meaning |
|---|---|
one_to_one | Each source row links one target, each target one source |
one_to_many | Each source row links many targets; each target links back to one source |
many_to_one | Each source row links one target; a target can be linked by many sources |
many_to_many | No limit on either side (default) |
Link rows
oxygen tables link invoices <row_id> \
--relation vendor \
--target-row-id <vendor_row_id> \
--live --jsonDefaults to dry-run; the preview shows which edges would be created and which would be archived by cardinality. MCP: oxygen_tables_link.
On the web, the record page renders each relation column with a server-backed search-picker; links also appear from the target row's side. Use the × control on a linked-record chip to unlink that exact pair.
Unlink rows and manage a relation
Unlinking removes one exact edge. It leaves both rows and the relation definition in place, so the same pair can be linked again later.
# Preview (default)
oxygen tables unlink invoices <row_id> \
--relation vendor \
--target-row-id <vendor_row_id> \
--json
# Apply after inspecting the preview
oxygen tables unlink invoices <row_id> \
--relation vendor \
--target-row-id <vendor_row_id> \
--approved --jsonManage the definition separately:
oxygen tables relations list invoices --json
# Rename either side or change cardinality; previews unless --approved is set.
oxygen tables relations update invoices vendor \
--display-name Supplier \
--inverse-display-name Invoices \
--cardinality many_to_one
# Archive the definition, its visible columns, and all active edges.
oxygen tables relations archive invoices vendor
oxygen tables relations archive invoices vendor --approvedA cardinality update is refused when existing links violate the requested shape; unlink the conflicting pairs first. Archiving retains the definition, columns, and edges in audit history while hiding them from active Tables and releasing the slugs for reuse. It never deletes either endpoint table or registers/unregisters a CRM object.
In the app, open Table relations → Manage relations to inspect active-link counts, rename both sides, change cardinality, or archive the definition. Opening a relation column's editor shows the target table, links-per-row, inverse column, and storage owner, with a direct Manage relation action.
MCP: oxygen_tables_relations and oxygen_tables_unlink, both with the table-relations widget.
CRM objects
Explicit CRM object relationships remain owned by Records and use the CRM relationship store. oxygen crm setup ships the standard relationships (companies.team, deals.company, deals.primary_contact, deals.stakeholders with a per-edge --role). Table reads hydrate both stores into the same linked-record cell shape, so relations created before this Tables-owned boundary continue to work.
Use the object-level commands when the relationship is intentionally part of the CRM model:
oxygen crm relationships define <object> <slug> --target-object <object> --live
oxygen crm relationships upsert <object> <row_id> --relationship <slug> --target-row-id <id> --live
oxygen crm objects describe <object> --json # relationships on an object
oxygen crm get <object> <row_id> --json # a record's inbound/outbound linksMCP: oxygen_crm_relationship_define, oxygen_crm_relationship_upsert.