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.
Read rows and grid state
These reads run no columns and cost 0 credits. Through MCP, search for the read you need, inspect its schema with oxygen_capabilities_schema, then call it through oxygen_capabilities_read.
| Read | Use it for |
|---|---|
oxygen_tables_query / oxygen tables query | Raw row values with filters, search, sorts or selected fields. Its optional format: "grid" returns the grid's summarized cell previews and per-cell run state. |
oxygen_tables_preview / oxygen tables preview | Compact rows and whole-table fill-rate summaries. include_cell_states adds state; summary_only omits row values. Its exact row count is separate from rates, which may be sampled (summary.statsSampled). |
oxygen_table_rows_page / oxygen tables rows page | A grid page with its native filter, sort, search, count mode and CRM scope. |
oxygen_table_fill_progress / oxygen tables fill-progress | Ingestion progress and actionable failure, separately from cell fill rates. |
oxygen_tables_cell_inspect / oxygen cells inspect | One cell's final value and provider attempt timeline. oxygen_tables_cell_history / oxygen cells history reads its recorded changes. |
Query's grid mode defaults to 50 rows and caps at 200. It accepts table, limit, cursor, include_stats, include_fill and max_chars. It refuses row_id, fields, filters/filter trees, search, sorts, formula-value options and nonzero offset; omit grid format for a filtered or projected raw read. Follow grid.nextCursor by passing it as cursor with the same table and limit. Preview and rows-page reads return their own nextCursor; preserve each read's filters, sort, search, projection and CRM scope when continuing. Cursors are native continuation positions, not a snapshot of a changing Table.
Grid's first page uses the reconciled registry row count when available, otherwise an exact count. Later pages skip counting and return a null count. rowCountMode identifies registry, exact or skipped; a registry count is stored metadata, not a count taken during this read. Rows page additionally accepts a count choice: exact counts the matching scope; estimate uses registry metadata only for an unfiltered scope and otherwise counts exactly; registry never counts and returns no total for a narrowed scope or unavailable metadata; skip returns no total.
include_stats adds optional whole-table column statistics and their count to Query's grid response. A failed or timed-out stats read leaves those fields absent. include_fill adds optional fill state only when the first page has no rows; it is omitted on nonempty or continuation pages. Neither option authorizes a run. Use action-run reads for durable run/items history, and verify an external destination before recording an uncertain effect's outcome.
Functions
Open Tables → Functions to browse reusable table computations. Each Function declares its inputs, the column steps to run, and the outputs it returns. Selecting a Function opens a detail panel with its published configuration, estimated credits per row and activity. View runs shows the versions and outcomes of previous calls.
The library contains Functions created in your workspace. Star a Function to keep it under Favorites; favourites are per person, per workspace. Use oxygen functions duplicate <function> to make an editable draft with the same inputs and outputs.
Choose Edit draft configuration to work in the familiar table grid. Define the input and output fields and order the steps in the configuration panel. Use draft rows to test the columns, then save, review and publish the configuration. Running paid test columns uses the normal preview and credit approval controls.
Publishing automatically updates future calls from every connected table. Each queued or running call keeps the version it started with. Past results stay unchanged, and draft edits stay separate from the published version. Publishing never raises a connected table's credit limit; if a new required input or a removed output breaks its mapping, correct that mapping before running it again.
The same lifecycle is available from the CLI:
oxygen functions list --json
oxygen functions describe <function-id> --json
oxygen functions runs <function-id> --jsonUse functions draft --definition-json '<configuration>' to create a draft, or include a Function ID to save its complete configuration. The JSON defines the backing table, name, inputs, step columns, outputs and limits; functions draft --help describes the fields. When editing an existing draft, pass --expected-draft-hash from inspection to detect concurrent edits. Inspect the returned function.draft.contentHash, then publish exactly that reviewed draft:
oxygen functions publish <function-id> --expected-draft-hash <hash> --jsonUse functions bind --help to connect another table's columns to the Function's named inputs and outputs. In the web app the same bind is a row under + Add column → Functions: pick your Function, select columns or nested JSON fields with the column reference picker, and choose where each output lands (a new column by default). Adding runs nothing and opens the column editor, where Run for first 10 rows shows the normal run preview. The configuration has no credit-cap field; the Function's existing per-row limit still applies. Adding without running spends nothing. Existing callables commands remain compatible. MCP exposes the same lifecycle through Manage Functions (oxygen_callables_manage). CLI --inputs-json and MCP input_mapping accept nested paths such as {"domain":"profile.company.domain"}; output mappings still name existing caller columns.
Each Function's three-dot menu offers Edit draft, Rename, and Delete. Renaming preserves its identity, connected callers and published execution. Deleting removes it from the Function library while keeping the backing table, rows and run history. The confirmation names every column that calls the Function; those columns stop running and keep the values they already wrote. Only the Function's own active runs block deletion. Select several Functions with their checkboxes to delete them together.
oxygen functions rename <function-id> "Enrich company" --json
oxygen functions delete <function-id> --json
oxygen functions delete <function-id> --yes --jsonThe first delete command previews the affected Function and its dependencies; --yes confirms removal after review. After renaming, inspect a fresh draft hash before saving or publishing.
Shape
| Field | Notes |
|---|---|
id | Stable table identifier |
name | Display name. The slug is derived from it and deduplicated workspace-wide (leads, leads-2), so duplicate names are allowed |
project | Optional project (folder) id or slug; defaults to general |
description | Free text |
columns | Ordered, typed; see Columns |
row_count | Maintained by the runtime |
web_url | Deep-link for visual inspection when returned by CLI/MCP |
Creating a table
Find tables with oxygen tables list --query "Acme" --projection summary --json. The name filter searches every table. Follow nextCursor with --cursor and the same filters to continue; --max-chars bounds each page. HTTP and CLI lists keep their full 200-entry default. MCP's oxygen_tables_list returns 50 compact entries within the client's result window.
Listed rowCount is null when it has not been measured. Use oxygen limits show --json (MCP: oxygen_limits_show) to find the largest table, or tables describe <table> --stats (MCP: oxygen_tables_describe with include_stats:true) to measure one 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"}]' \
--jsonlist, 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"}]' --jsonUse 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. A table with a large history (millions of rows of runs and changes) is purged in stages: --now --yes answers status: "purging", the table is hidden at once and can no longer be restored, and the sweep deletes its history in batches before dropping it. tables get <table> --include-archived answers table_not_found once it is gone. An archived, scheduled or purging table no longer counts as a full Table, but its rows count toward the workspace total until the purge finishes. 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.
Cost: deleting, restoring and purging are free. None of them charges credits (credits_used: 0), and the --now approval preview says so for the approved purge in credits (estimated_credits: 0). columns delete --now works the same way.
Custom CRM objects: 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 14-day restorable timer (dry-run by default; --live requires --confirm, and --keep-table deregisters the object while keeping the table). This is free as well. 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.
Importing a Supabase database
Supabase import is a free, read-only source operation that writes snapshots into Oxygen. Create a dedicated PostgreSQL role with read access only to the user schemas you need; Oxygen refuses the connection if the role has database-level elevation or write privileges. Managed/system schemas such as auth, storage, and realtime cannot be selected.
Run this in the Supabase SQL editor, replacing the password and repeating the schema grants for each user schema you want to import:
CREATE ROLE oxygen_reader
WITH LOGIN PASSWORD 'replace-with-a-long-random-password'
NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION;
ALTER ROLE oxygen_reader SET default_transaction_read_only = on;
GRANT CONNECT ON DATABASE postgres TO oxygen_reader;
GRANT USAGE ON SCHEMA public TO oxygen_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO oxygen_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO oxygen_reader;URL-encode the generated password when constructing the PostgreSQL URL. If the connection check reports inherited write or schema-creation privileges, revoke those privileges in Supabase before connecting; Oxygen will not store a credential it can use to write.
The web path is New table → Supabase. Connect the database, select base tables, review the create/refresh and row-limit plan, and start the durable import. One source table creates one Oxygen Table. The status page remains available at /tables/imports/<import-id> and links to every child Table.
The CLI keeps the connection string out of arguments and shell history by reading it from stdin:
read -rsp "Supabase read-only database URL: " SUPABASE_DATABASE_URL
printf '%s' "$SUPABASE_DATABASE_URL" | oxygen supabase connect --database-url-stdin --schemas public --json
unset SUPABASE_DATABASE_URL
oxygen supabase catalog --json
oxygen supabase plan --tables public.customers,public.signups --json
CATALOG_FINGERPRINT=copy_the_catalog_fingerprint_from_the_plan
oxygen supabase import \
--tables public.customers,public.signups \
--catalog-fingerprint "$CATALOG_FINGERPRINT" \
--json
IMPORT_ID=copy_the_import_id_from_the_start_response
oxygen supabase get "$IMPORT_ID" --jsonRefresh by planning and importing the same source again. A primary key gives Oxygen stable source identity: matching rows update in place, Oxygen-only columns remain untouched, and source rows missing from a completed refresh receive a source-deleted timestamp instead of being removed. A table without a primary key supports one snapshot only. Added, removed, or type-changed source columns refuse refresh so schema drift cannot silently corrupt the destination. Catalog, plan, import, status, cancel, and retry all charge 0 credits and never write to Supabase.
One import carries at most 50 source tables; select the rest in a second import. A source table PostgreSQL has never analyzed reports no row estimate, and the plan says so rather than guessing zero — the worker measures the real count against your plan's per-table limit before it writes anything.
The import itself is always free, but it is not the only thing that runs. If a destination Table already has a standing auto-run subscribed to imports, its columns run over every imported row and may spend credits. The plan discloses this before you start: credits.standing_auto_run_may_spend, a summed credits.standing_auto_run_max_credits when every subscribed config declares a ceiling, and the affected tables in effects.standing_auto_run_tables.
Capacity and recovery
Storage limits follow the plan size: retained rows per Table, retained Table rows across the workspace, and a hard limit on net-new PostgreSQL Workspace Table growth with a warning below it. Today every plan size up to Oxygen $499 allows 3,000,000 rows per Table, 25,000,000 across the workspace, a warning at 20 GiB and a limit at 30 GiB; Oxygen $999 and $1,999 allow 50,000,000 and 100,000,000 rows across the workspace and 60 GiB and 120 GiB, with 3,000,000 rows per Table until their capacity test passes (see the pricing reference). The byte counter includes Table heap, indexes, and TOAST; S3/object storage is separate. Run oxygen limits show --json for your plan's limits, every plan size's limits and current, timestamped usage, and see Billing & credits for the complete boundary behavior.
At a hard limit nothing is deleted: reads, previews, queries, exports, row deletion, permanent Table purge, and non-growing updates remain available, and only new growth is refused. 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.