Columns
Typed actions that produce row-level values, state, and provenance.
A column is a typed action on every row of a table. Some columns hold source data; others compute values, call AI, call provider tools, enrich records, or link workflow state. Every executable column writes cells with status, cost, provenance, and history.
Column kinds
| Kind | Produces | Cost | Determinism |
|---|---|---|---|
manual | Imported, pasted, or entered value | 0 | Static |
source | Provider-sourced or imported fact | 0 or provider-defined | Source-defined |
formula | Computed from other cells in the row | 0 | Deterministic |
research | The Web Research Agent: live web searches — or one fetched page — + an LLM answer with its sources, per row | By depth: 2.1 / 5.5 / 10 credits typical per row | Non-deterministic, evidence-bound |
ai | LLM call, no web access | Credits per call | Non-deterministic |
tool | Provider tool call (catalog) | Varies | Provider-defined |
enrichment | Managed provider waterfall for one identity intent: work email, personal email, mobile phone, LinkedIn URL, or email verification | Per provider attempt | Provider-defined |
relation | Durable links to rows in any other Table; plain tables stay outside CRM (Relations) | 0 | Deterministic |
bind | Resolves each row to a CRM Record by identity matching | 0 | Deterministic |
lookup | Current value pulled from another table by matching a key (first match, count, exists, or aggregate) | 0 | Dynamic read + materialized runs |
workflow | Workflow-owned status or output | Depends on workflow | Workflow-defined |
system | Oxygen-managed metadata | 0 | System-defined |
Lookup vs. relation. A relation is a durable, two-way row link; its definition and edges stay in Tables unless it fills a relationship explicitly defined in CRM. A lookup is a computed join by a normalized value such as id, email, or domain. Use a relation for “these two rows are connected”; use a lookup for “show the current company attributes for this contact's domain.”
Lookup values are read-through and current, and filters and sorts resolve live via a join against the source rows. Past a row-count cap the source table is too large to join per-page without breaking cursor pagination, so filtering and sorting fall back to the last materialized cell — run the column to refresh that materialization. If current cross-table ordering is the core data model, use a relation and store the sortable fact on the table that owns it.
Data types
--data-type accepts exactly five storage types:
data_type | Holds |
|---|---|
text | Strings |
numeric | Numbers |
boolean | True/false |
jsonb | Structured JSON — the default for AI and tool outputs |
timestamptz | Timestamps with timezone |
Richer column behavior is layered on top of these, not new data types:
- Semantic type (
semantic_type) refines how a value is rendered and filtered:email,phone,url,domain,markdown/rich_text,single_select,multi_select,status,date, and similar. A select column istext+ a select semantic type. - Kind (table above) is the column's behavior —
relationis a kind, not a data type. Relation columns are never added withcolumns add;oxygen tables link --tocreates one for you, oroxygen tables relatedefines one by hand. See Relations.
columns retype converts a manual or source column to a different storage type, coercing every stored value in one transaction — the escape hatch for a column imported as text that should have been timestamptz. Both the current and target type must be one of text, numeric, boolean, jsonb, timestamptz; every other kind is refused with column_not_retypeable. Every non-null value must coerce or the whole retype is rejected with column_retype_failed and a sample of the offending rows, so a table is never left half-converted. --dry-run shows the row counts and samples first.
Adding a column
oxygen columns add <table-id> --label <label> --key <key> --kind <kind> --data-type <type> --jsonManual
oxygen columns add <table-id> --label "Notes" --key notes --kind manual --data-type text --jsonFormula
oxygen columns add <table-id> \
--label "Fit Score Number" \
--key fit_score_number \
--kind formula \
--data-type numeric \
--definition-json '{"expression":"path(fit_score, \"score\")"}' \
--jsonUse formulas to extract scalar fields from JSONB AI or tool outputs before downstream steps use them.
Research (Web Research Agent)
A research column — the Web Research Agent in the app — answers one question per row from the live web — what a company
sells, whether they raised, which tools they run. Anything you would otherwise
look up by hand belongs here rather than in an ai column.
In the app this is the Web Research Agent column in its Agent mode, switched
with the toggle at the top of the column editor. Its other mode, Keyword search,
skips the model and returns the search results themselves (title, link, snippet)
for less per row. From the CLI that mode is a tool column on the managed search:
oxygen columns add <table-id> --label "Keyword search" --kind tool \
--definition-json '{"toolId":"serper.search","inputMapping":{"q":{"type":"template","value":"{{company_name}} funding"}}}' \
--jsonoxygen tools search web --json prices both modes under the Web Research Agent entry's modes.
Keyword search can be narrowed by country (gl), language (hl) and time range (tbs)
as literal inputs; oxygen tools enums get serper country --json lists the accepted codes
with their names (language and time_range work the same way). In the app these are
dropdowns in the column editor that default to Any.
oxygen columns add <table-id> --kind research \
--label "What they sell" \
--prompt "What does {{company_name}} ({{company_website}}) sell, and to whom?" \
--jsonThere is no model or search engine to choose. Oxygen runs the search itself, feeds the retrieved pages to the model, and writes a fixed shape:
{
"answer": "Payroll and compliance software for European SMBs",
"found": true,
"confidence": "high",
"sources": [{ "title": "Acme — Pricing", "url": "https://acme.example/pricing", "engine": "exa" }]
}sources comes from the pages Oxygen actually fetched, not from the model: the
model cites evidence by position and Oxygen resolves it, so a research cell cannot
invent a citation. Pull the answer out with a formula — path(what_they_sell, "answer") —
or filter on found.
Research columns fail closed. If a row has no findable evidence the cell errors
with grounding_unavailable instead of guessing, and only the searches that ran are
billed — the model is never called. That usually means the search query needs help:
oxygen columns add <table-id> --kind research \
--label "Funding stage" \
--prompt "What funding stage is {{company_name}} at?" \
--research-query "{{company_name}} funding round raised" \
--research-domains techcrunch.com,crunchbase.com \
--jsonBy default the model may summarize and combine what the sources say, while never
asserting anything they do not support. --research-mode strict forces
verbatim-only answers (use it when you need a figure or identifier quoted exactly);
--research-mode estimate lets the model reason to a number from the evidence, for
columns whose job is an estimate. --research-results <n> (1–25) sets how many
search results to ground each row on; --reasoning-level low|medium|high spends
more on the answer itself.
In the app these stay editable in the column's definition: open the column editor
and switch it to JSON (under webSearch: query, mode, result count, and the
domains to include or exclude).
Ask for every field in one column. A prompt that names its sections —
Return the following sections: then Enterprise plan: yes or no and
Cheapest paid plan: price and period — answers all of them in one cell, under
answer.<section>. One row, one read, one charge. A second research column over the
same page pays for the same search again.
AI column or Web Research Agent
An ai column never searches the web. Turning web search on for one makes it a Web
Research Agent, which is a conversion: the column is rebuilt under the same key,
label and position with its prompt, inputs and model tier, and its results are
cleared. columns update refuses to flip it on (column_conversion_required);
convert instead, and convert back the same way:
oxygen columns convert <table-id> summary --to agent # prints what would change
oxygen columns convert <table-id> summary --to agent --yes # converts, clearing results
oxygen columns convert <table-id> summary --to ai --yes # back to an AI column, no webThe plan shows the per-row price before and after and any columns that read this one.
A column the table's standing auto-run runs cannot be converted until the auto-run is
disabled (oxygen tables auto-run disable <table-id>), so the new kind never runs at its
new price without being asked.
columns add of an ai column with webSearch.enabled creates a research column
and says so in its materialized_as field. AI columns saved with web search before
2026-09-26 keep running unchanged; the editor offers them the conversion.
Research depth
--research-depth sets how hard each row searches. The typical cost per row is
below; every row reserves a higher maximum, and a dry run quotes both for your column.
| Depth | What one row does | Typical credits / row |
|---|---|---|
quick | One search, then reads the top result, then one answer on the column's tier | 2.1 on Oxygen Fast |
standard | An agent plans a few searches and page reads on Oxygen Fast (default for new columns) | 5.5 |
deep | More searches and whole-site subpages on Oxygen Balanced | 10 |
Standard and deep always run on the tier their depth names and cost the same whatever
--reasoning-level the column stores; the stored tier prices quick only.
oxygen columns add <table-id> --kind research \
--label "Enterprise plan" \
--prompt "Does {{company_name}} offer an enterprise plan?" \
--research-depth deep \
--json
oxygen columns update <table-id> enterprise_plan --research-depth quick --jsonStandard and deep rows cite only pages Oxygen actually fetched, and when the evidence
does not settle the question the answer is found: false with low confidence rather
than a confident "no". If a row's time or budget runs out first, it answers from
what it found (found: false, validation.fallback says why) instead of failing.
Each search and page read is recorded in the row's run provenance, not in the cell.
Columns created before depth existed stay on quick until you
change them; templates from columns catalog run quick too. In the app, pick the
depth in the column editor.
Dry-run first — it is free and prints the exact estimate.
Read one exact page per row
When a column already holds the page — a competitor's pricing page, a job posting, an event page, a 10-K — point the research column at it instead of searching:
oxygen columns add <table-id> --kind research \
--label "Plan count" \
--prompt "How many pricing plans does this page list? Return the count and the plan names." \
--research-url pricing_url \
--json--research-url <column> reads the URL in that column for each row. The page's
content is the only evidence, the cell's sources is that page, and a row with no
valid http(s) URL fails closed instead of searching for the prompt text. Fetch mode
is strict by default (nothing the page does not state; unsupported fields null);
--research-mode estimate relaxes it. --research-engine firecrawl|linkup|exa pins
the fetch provider, and a .pdf URL is read by the PDF-capable lane first. The row
costs one page fetch plus the model.
Column templates
For the common page, text and cleanup jobs there is a template catalog: each template carries a maintained prompt or formula, a typed answer schema, its grounding (a fetched page, a web search, or the row's own text) and a per-row credit estimate.
oxygen columns catalog --category extract
oxygen columns catalog --category normalize
oxygen columns catalog --search pricing
oxygen columns add <table-id> --prompt-key pricing_plan_summary_v1 --input url=pricing_url --json
oxygen columns run <table-id> pricing_plan_summary --limit 1 --dry-run --json
oxygen columns add <table-id> --prompt-key clean_company_name_v1 --input company=company_name --json--input <name>=<column> binds each input the template declares (url for page
templates, company / industry / domain for search templates, description /
address for text templates); --input-mapping '{"url":{"column":"pricing_url"}}'
is the JSON spelling. A research or AI template's answer sits under answer in the
usual {answer, found, confidence, sources} cell, so
{{pricing_plan_summary.answer.plan_count}} reads one field (the new column is keyed
from the template title, here pricing_plan_summary, unless you pass --key).
--category picks the job. extract is the page, filing and text extractions
(families: pricing page, job posting, events, 10-K filings, website facts, market,
investor portfolio, plain-text extraction). normalize and organize are the
free formula cleanups an imported list usually needs first —
clean_company_name_v1, clean_job_title_v1, email_type_v1 and
list_from_text_v1 each add a clean column and leave the imported one as it is, cost
0 credits, and need no run because a formula evaluates on read (the fill-empty
fields an enrichment bundle adds are the exception: they are stored, filled by
a run only where empty, and editable). summarize is
judgement over what the row already holds, such as company_b2b_b2c_classification_v1.
The same catalog is the MCP tool oxygen_columns_catalog (templates are the
prompt_key parameter of oxygen_columns_add) and the Extract / Normalize /
Organize / Summarize sections of the web add-column panel, which ask which column
to bind and show the credits per row before adding.
The catalog's second section, --category company, answers the standard questions
about a company from its domain or name column — 48 templates in seven families:
web presence (LinkedIn, Crunchbase, Glassdoor, Yahoo Finance, YouTube, Instagram and
careers page lookups, website from name, name from domain, phone, email), corporate
structure (founders, parent company, subsidiaries, organization type, ever been sold,
founding year, came out of stealth), funding (every round, total raised, last round,
market cap, valuation trend), footprint (HQ in a location, US states, cities,
continents, store and location counts), business model (is SaaS / retail / B2B,
customer type, ICP from website, offers demos, data breach, normal or inferior good,
case study), tech (cloud provider, CRM from tech stack, domain registration date) and
market (industry, NAICS codes, blog keywords, competitors in a region, websites in an
industry).
oxygen columns catalog --category company --family corporate_structure
oxygen columns add <table-id> --prompt-key company_founders_v1 --input domain=domain --json
oxygen columns add <table-id> --prompt-key hq_in_location_v1 --input domain=domain \
--input-mapping '{"location":{"literal":"Berlin"}}' --jsonThe kind behind the key decides the cell. A research question writes the usual
{answer, found, confidence, sources} cell with a typed answer: a yes/no question
answers answer.verdict next to its evidence field (gives_demos_v1 →
answer.demo_url), a list question answers an array (answer.founders), a
classification answers one of its listed values (cloud_provider_v1 →
answer.provider). A page lookup (kind: search) is a managed web-search tool column
at a fixed price and no model: the cell is the first result's URL, or the result list
for open_jobs_linkedin_v1 and websites_in_industry_v1. Bind a column with
--input <name>=<column>; where a key takes a value (location, region,
industry) pin it with --input-mapping '{"<name>":{"literal":"…"}}'. The web
add-column panel lists the same keys under Company research.
A template that reads the web is quoted as two figures, and every surface quotes
the same two: ~3.1 credits/row (up to 3.6) in the catalog and the picker is
expected_credits_per_row: 3.1 and estimated_credits_per_row: 3.6 in
columns run --dry-run. The expected figure assumes the first evidence lane
answers (the page fetch, the news feed, or the primary search); the higher figure
is what a row reserves so the fallback lane can run when the first returns
nothing. Unused reserve is never spent.
The People section of the same catalog (--category people) answers questions about a
person from one profile lookup: add --preset person_enrich once, then bind each template's
profile input to its payload column (--input profile=person_enrich) for skill set, grad
school, recent graduation, location, current company and domain, industry or company history,
and fit for a job opening at 1.5 credits per row with no web call. The appearance templates
(any public appearance in one search, or events, keynotes and podcasts separately, all
within the last 12 months; GitHub profile) search the web by
full_name with strict evidence, so a person with nothing verifiable reads found: false.
A LinkedIn URL from a name and company or a work email stays with the identity-validated
--capability linkedin_url waterfall rather than a search template.
AI
An AI column has no web access. It reasons over the row's existing values. If a
prompt asks it to check a website or recall a fact about a company, it will answer
from the row and can fabricate — use a research column instead, or add
webSearch: {"enabled": true} to the definition.
oxygen columns add <table-id> \
--label "ICP Fit" \
--prompt "Score {{company_name}} ({{domain}}) against our ICP. Return tier and one-sentence reason." \
--jsonReference other columns in the prompt as {{column_key}} — that is the whole
authoring step. --kind ai and the data type are implied, and no input mapping is
needed. For OXYGEN's curated copy templates, swap --prompt for --prompt-key <key>
plus --input-mapping; that path materializes a maintained prompt and output schema
instead of your own text.
AI outputs are stored as JSON envelopes. Use a formula column or columns materialize when downstream tools need one specific field.
Classify
Use Add column → Classify when each row needs a label, yes/no answer, or an ordered score. It reads the row's existing data; for ICP fit, enable Use workspace knowledge so it also reads your saved ICP. Jev answers the column for 0.075 credits per row; no separate API key is needed.
oxygen columns add <table-id> --label "ICP Fit" --score "low,medium,high" \
--prompt "How well does {{company_name}} fit our ICP? Description: {{description}}. Employees: {{employee_count}}. High = clear fit, medium = plausible but incomplete, low = clear mismatch. Do not infer missing facts." \
--grounding workspace --json
oxygen columns run <table-id> icp_fit --all --dry-run --jsonReplace the {{...}} names with your table's actual column keys; name every evidence column the decision should read. The score is ordinal, not a 0–100 percentage. Each result includes score, level, confidence, uncertain, and reason. level is nearest to the numeric score, so it can differ from the label with the largest probability. The classifier returns confidence separately from those probabilities. A result below the default 0.85 confidence floor keeps its best guess and marks uncertain: true; review those rows and spot-check borderline cases and rows with missing evidence even when they are not flagged. Confidence does not measure whether the source facts are complete. To execute after the cost preview, rerun with --background --approved --max-credits <cap>. The add and run results provide the exact filter for uncertain rows.
Tool
oxygen columns add <table-id> \
--label "Person Enrichment" \
--key person_enrichment \
--kind tool \
--data-type jsonb \
--definition-json '{"version":1,"mode":"native","toolId":"blitzapi.person_enrich","inputMapping":{"person_linkedin_url":{"type":"column","columnKey":"linkedin_url"}}}' \
--jsonOne toolId, one column. Find it with oxygen tools search "<what you want>", then oxygen tools get <tool-id> --json for the input schema, availability, and current price; every provider in the catalog is listed in the provider reference. columns add writes the definition and never spends.
When one provider input is an object, compose it from row columns inside a
literal. Ordinary values stay literal; an explicit nested column ref is resolved
for each row, including an optional path into a JSON cell:
{
"properties": {
"type": "literal",
"value": {
"email": { "type": "column", "columnKey": "email" },
"firstname": { "type": "column", "columnKey": "first_name" },
"lead_source": "OXYGEN"
}
}
}The equivalent template form accepts the same object/array shape with values
such as "{{email}}"; an exact placeholder preserves the cell's scalar or JSON
type instead of converting it to text.
Lookup
oxygen columns add <table-id> \
--label "Company" \
--key company \
--kind lookup \
--data-type jsonb \
--lookup-table <companies-table-id> \
--lookup-match company_id=supabase_id \
--lookup-normalize exact \
--lookup-return company_name,domain \
--json--lookup-match <local>=<source> names the join: the local column on this table, then the column to match in the source table. --lookup-mode is first-match (default; pulls the --lookup-return columns), count, exists, or aggregate; --lookup-order <column:dir> breaks a first-match tie. --lookup-normalize is exact, lower-trim (default), email, domain, or linkedin. Lookups are free: reads show current source values, and a run materializes them in large, observable batches.
Enrichment presets
oxygen columns add <table-id> --preset person_enrich --json
oxygen columns add <table-id> --preset company_enrich --input domain=website --jsonOne flag adds a whole bundle instead of one column: person_enrich creates 14
columns — a single paid LinkedIn profile lookup (1 credit/row) plus 13 free
formula extractors (headline, bio, location, followers, full/first/last name,
job title, current company, current company LinkedIn URL, job start date,
education, school name). Followers fills only on rows the LinkedIn Scraper
answers: Blitz, the first lane, publishes no follower count, so most rows leave
it empty. company_enrich has the same shape: one company
lookup from a domain or a company LinkedIn URL (Blitz first, the LinkedIn
Scraper only when Blitz has no match; about 1 credit a company, and a miss
costs nothing) that keeps the whole profile in its JSON column, plus 8 free
extractors — name, domain, LinkedIn URL, headcount, industry, description,
founded year and HQ country. A domain-only row looks up its LinkedIn URL
first, at no charge. It auto-binds its LinkedIn slot to a
current_company_linkedin_url column when one already exists — so applying
person_enrich then company_enrich fills the person's company profile from
the URL the person lookup found.
For ad-audience uploads, hash emails with a free formula column instead:
email_hash(email) (SHA-256) or email_hash(email, "md5"). Oxygen infers the identity column and only
--input <slot>=<column> overrides it. Creating the columns is free and
re-running the same preset skips what already exists; running the paid ones is
not free, so dry-run first — or run oxygen enrichment catalog --json up front to
see every preset's category, description, provider count, and per-row credit
estimate before adding anything. The same presets are the preset parameter on
the MCP tool oxygen_columns_add, and the Person enrichment / Company
enrichment / Tech stack / Funding / Revenue (estimate) / Hiring
signals rows in the web add-column panel.
company_enrich has no field choices. Funding, revenue, open roles and
technologies are bundles of their own, each one paid lookup column that fills
its own columns: --preset company_funding (total raised, latest round,
number of rounds and investors; Apollo, then People Data Labs, then
LeadMagic), --preset company_revenue (an annual revenue estimate; Apollo,
then People Data Labs), --preset company_hiring (up to ten open roles with
a count; Blitz, then LeadMagic) and --preset company_tech_stack (the
technologies a company runs). Each is quoted per row before you add it, as
the ceiling if every provider in its chain is tried. For one company, or any
other field, run oxygen find company --fields <fields> (--list-fields
prints the catalog with each field's per-row credits). A table that already
has an earlier company enrichment column keeps it exactly as it is.
Enrichment helper
oxygen enrich-column preview <table-id> \
--capability work_email \
--linkedin-url-column linkedin_url \
--company-domain-column domain \
--limit 10 \
--jsonThe helper creates or reuses an enrichment column and picks the provider waterfall. --capability is work_email, personal_email, mobile_phone, linkedin_url, or verify_email (default mobile_phone); preview makes no provider call and spends nothing. To create the same column now and run it later, oxygen columns add <table> --capability <capability> seeds it — same kind, jsonb cell, label, key and provider waterfall as the app's + Add column picker — without running anything. It maps the column's inputs from the table's own columns, the way preview does; an input it cannot find comes back under warnings (enrichment_inputs_unmapped) with the oxygen columns update <table> <column> --input-mapping command that maps it, and until then columns run quotes 0 credits and refuses to queue a run that could only find nothing. The first four FIND a value the row is missing; verify_email GRADES an email it already has — see Email verification. See also Waterfalls.
Referencing other columns
Every column that reads another column uses one grammar: {{column_key}}. It works
in an AI or research prompt, in a research --research-query, in a custom-HTTP URL
or body, and in a tool column's template inputs. In the app, the + Column button
beside each of those fields inserts one, and a reference renders as a chip you can
click to open the column it points at.
{{domain}} # the whole cell
{{research.answer}} # a field inside a JSON cell
{{research.sources.0.url}} # an array elementThree rules are worth knowing:
- Naming a column in prose does nothing. "Look at the scraped page content" sends
the model those words, not the value. Only a
{{token}}— or an entry ininputMapping— actually delivers the cell. - A JSON cell interpolates as JSON.
{{page_content}}on ajsonbcolumn renders the serialized object, so a whole scraped payload can go straight into a prompt. - Unknown references are rejected before the write.
oxygen columns add,columns update,tables create, and every MCP equivalent refuse a definition whose{{token}}names no column, withinvalid_prompt_columnand the near-miss keys. The web editor saves anyway — it autosaves while you type — but paints the chip red andcolumns runrefuses the paid run until it resolves.
inputMapping is a second, narrower channel: named inputs delivered to the model
alongside the prompt. Use it for the things a prompt cannot name on its own —
literals and workspace context — and read them by the same {{name}} grammar:
oxygen columns update <table-id> icp_fit \
--input-mapping '{"icp":{"type":"context_profile","path":"icp"}}' \
--prompt "Score {{company_name}} against this ICP: {{icp}}." \
--jsonRow columns belong in the prompt. Putting one in inputMapping instead still works —
the value reaches the model — but nothing links the two, so the app cannot show the
dependency and a reader cannot tell which input the prompt meant. columns add and
columns update warn when an input is never referenced.
Run conditions are the exception: --run-condition takes a formula over bare column
keys (eu_israel = true), not a {{token}} template, because it is evaluated by the
formula engine rather than interpolated into text.
oxygen columns deps <table-id> prints the whole graph — what each column reads, what
reads it, and any reference that resolves to nothing.
Renaming a column does not rewrite the references pointing at it. columns rename
warns which columns read the old key; update them, or they fail on their next run.
Running a column
Dry-run first. It spends nothing and prints the model tier (Oxygen Fast, Balanced or Max), the credit estimate, the run-condition posture, and — for an AI column — the prompt rendered against one real row:
oxygen columns run <table-id> <column-key> --limit 1 --dry-run --jsonThen run a small pilot, then the batch. A paid column always runs durably in the background and needs --approved plus a --max-credits ceiling:
oxygen columns run <table-id> <column-key> --limit 1 --approved --max-credits 10 --json
oxygen table-runs create <table-id> --column <column-key> --limit 100 --approved --max-credits 50 --json--limit runs the next N rows whose target cell is still empty (--force runs the first N regardless); it defaults to 10, and inline deterministic runs cap at 25. --all requires --background for a live run; with --dry-run it previews the background run without queueing it.
See Runs, Modes, Approvals, and pricing reference for what each kind costs.
Maintenance
rename, reorder, archive, delete, restore, retype, deps, materialize, update, and rerun are the rest of the group — see the CLI reference. Three of them have sharp edges.
materialize copies JSON paths out of a result column. The fixed work_email preset creates email, email_provider, email_status, and email_enriched_at; it never binds to an existing column named work_email. Use --mappings-json when you need to write into a specific column.
oxygen columns materialize <table-id> person_enrichment --preset work_email --jsonrerun is the narrow paid AI-only path for one cell. --dry-run first, then a required --max-credits; --from-review-id threads a message review's feedback into the regeneration prompt.
oxygen columns rerun --table <table-id> --column <column-key> --row <row-id> --dry-run --json
oxygen columns rerun --table <table-id> --column <column-key> --row <row-id> --max-credits 10 --jsonupdate merges, it does not replace. --definition-json shallow-merges, so it can add or overwrite keys but never remove one. Name the keys to remove with --definition-unset — everything you do not name is left as it is, including keys another surface wrote since you last read the column:
oxygen columns update <table-id> <column-key> \
--definition-json '{"prompt":"Qualify ICP fit","credentialMode":"hosted"}' \
--definition-unset model,modelId --jsonA lookup column's output type has to match its mode (count/aggregate need numeric, exists needs boolean, multi-column first_match needs jsonb). Change the mode and the type in one request — the cached cells are discarded and re-resolved on the next read:
oxygen columns update <table-id> open_invoices \
--definition-json '{"sourceTable":"invoices","match":{"sourceColumn":"company_id"},"inputMapping":{"match_value":{"type":"column","columnKey":"company_id"}},"mode":"count"}' \
--data-type numeric --jsonDiscarding cached cells is specific to lookup: on a manual or source column, oxygen columns retype coerces every stored value instead of dropping it.
Related
- Cells — per-cell state model.
- Relations — relation columns and CRM relationships.
- Waterfalls — provider fallback for common enrichment jobs.