OXYGENOxygen/ Docs
Data

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.

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 companies
Linking "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 --approved

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

FlagWhat it does
--on <column>Match on this column instead of the one Oxygen picked. Accepts the column's name or key.
--create-missingAlso create target Table rows for native links, or CRM Records when filling an existing CRM relationship. Free.
--approvedApply. 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 dedupe and 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 companies fills the standard Company relation 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 --json

This 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_definitions and ox_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).

ValueMeaning
one_to_oneEach source row links one target, each target one source
one_to_manyEach source row links many targets; each target links back to one source
many_to_oneEach source row links one target; a target can be linked by many sources
many_to_manyNo limit on either side (default)
oxygen tables link invoices <row_id> \
  --relation vendor \
  --target-row-id <vendor_row_id> \
  --live --json

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

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 --json

Manage 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 --approved

A 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 links

MCP: oxygen_crm_relationship_define, oxygen_crm_relationship_upsert.

  • Columns — the other column kinds.
  • Tables — raw, object-backed, and list-backed tables.
  • Approvals — dry-run/live doctrine.

On this page