OXYGENOxygen/ Docs
Data

Tables

Typed GTM datasets and the audit surface for row-level work.

A table is a typed dataset inside an organization. Rows hold GTM entities such as accounts, contacts, leads, signups, product events, or campaign state. Columns add behavior: enrichment, scoring, research, formulas, provider actions, and workflow links. Cells carry the output and provenance for each row.

Shape

FieldNotes
idStable table identifier
nameDisplay name. The slug is derived from it and deduplicated workspace-wide (leads, leads-2), so duplicate names are allowed
projectOptional project (folder) id or slug; defaults to general
descriptionFree text
columnsOrdered, typed; see Columns
row_countMaintained by the runtime
web_urlDeep-link for visual inspection when returned by CLI/MCP

Creating a table

oxygen tables create "Q1 TAM" \
  --columns-json '[{"label":"Company Domain","key":"domain","data_type":"text","semantic_type":"company_domain"},{"label":"Company Name","key":"company_name","data_type":"text"}]' \
  --json

list, describe, preview, query, export, duplicate, rename, move, archive, views, and webhook are the free rest of the group; dedupe, link, promote, and schedule set are paid and preview before they spend. Every command and flag is in the CLI reference.

Writing rows

oxygen tables insert <table-id> --rows-json '[{"domain":"acme.com"}]' --json
oxygen tables upsert <table-id> --key domain --rows-json '[{"domain":"acme.com","company_name":"Acme"}]' --json

Use upsert for idempotent writes against a stable natural key. Use tables import --background for file-backed or generated batches that should run durably in the worker.

Deleting and restoring (the trash-can lifecycle)

delete is reversible by default: it archives the table immediately (hidden from every list and query), frees its slug for reuse, and schedules the physical purge for 14 days later (--purge-after <days> to tune, 0 = next background sweep). Until the purge runs you can undo everything with restore, which also reclaims the original slug when it's still free. Columns share the exact same lifecycle via columns delete / columns restore.

oxygen tables delete <table-id> --reason "junk from testing" --json   # archive + 14-day purge timer
oxygen tables list --include-archived --json                          # the restorable trash, with purge dates
oxygen tables restore <table-id> --json                               # undo: cancels the purge, reclaims the slug
oxygen tables delete <table-id> --now --json                          # immediate purge: previews and refuses...
oxygen tables delete <table-id> --now --yes --json                    # ...until explicitly approved. Permanent.

The purge (scheduled or --now --yes) is the only operation that destroys data: the physical Postgres table, its rows, columns, and run history are gone, and the only durable record is the operation event carrying a pre-destruction snapshot (visible in Observability). Scheduled purges run at or shortly after purge_scheduled_at — the background sweep visits each workspace every few minutes. archive remains the lighter verb: it only hides the table, with no purge timer and no end-state — use delete when you want cleanup with a deadline. A table backing a CRM object refuses tables delete. For a custom object, delete the object instead: oxygen crm objects delete <object> removes it and schedules the backing table on the same restorable timer (dry-run by default; --live requires --confirm, and --keep-table deregisters the object while keeping the table). The standard objects — companies, people, and deals — are permanent and are refused by that command too; tables archive is the only way to hide one.

In the web app, each active table in the left rail carries a Table actions menu: Rename, Duplicate, Move to folder, Copy link, Open in new tab, Export CSV, Delete. Archived and delete-scheduled tables stay out of that rail, so recovery is tables list --include-archived then tables restore through the CLI or MCP. Deleting from the web only ever archives — it never starts a purge timer.

Large file imports

In MCP, call oxygen_cli_file_import_prepare first so large files stay out of the assistant's context window. In a terminal:

oxygen tables import <table-id> --file leads.csv --upsert-key domain --background --json
oxygen tables import --create "From CSV" --file leads.csv --background --json

--upsert-key makes a re-import update matched rows instead of duplicating them. --create <name> infers the columns from the file.

Capacity and recovery

Every workspace has the same infrastructure guard: 3,000,000 retained rows per Table, 25,000,000 retained Table rows across the workspace, a PostgreSQL warning at 20 GiB, and a hard limit on net-new PostgreSQL Workspace Table growth at 30 GiB. The byte counter includes Table heap, indexes, and TOAST; S3/object storage is separate. Run oxygen limits show --json for current, timestamped usage and see Billing & credits for the complete boundary behavior.

At a hard limit, reads, previews, queries, exports, row deletion, permanent Table purge, and non-growing updates remain available. archive and a default scheduled delete retain the physical table until purge, so use the preview-gated tables delete <table> --now --yes only when immediate, irreversible space recovery is intended.

Table-first rule

External writes mirror useful state back into the originating table. Provider record ids, sync status, errors, timestamps, run ids, and costs should be visible from cells and runs. The table is the audit log.

CSV, JSON, and XLSX files are import/export artifacts. Don't make them the durable state.

  • Columns — column kinds and run behavior.
  • Cells — per-cell state and history.
  • Runs — running a column or workflow across rows.

On this page