OXYGENOxygen/ Docs
Data

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

KindProducesCostDeterminism
manualImported, pasted, or entered value0Static
sourceProvider-sourced or imported fact0 or provider-definedSource-defined
formulaComputed from other cells in the row0Deterministic
researchThe Web Research Agent: live web searches — or one fetched page — + an LLM answer with its sources, per rowBy depth: 2.1 / 5.5 / 10 credits typical per rowNon-deterministic, evidence-bound
aiLLM call, no web accessCredits per callNon-deterministic
toolProvider tool call (catalog)VariesProvider-defined
enrichmentManaged provider waterfall for one identity intent: work email, personal email, mobile phone, LinkedIn URL, or email verificationPer provider attemptProvider-defined
relationDurable links to rows in any other Table; plain tables stay outside CRM (Relations)0Deterministic
bindResolves each row to a CRM Record by identity matching0Deterministic
lookupCurrent value pulled from another table by matching a key (first match, count, exists, or aggregate)0Dynamic read + materialized runs
workflowWorkflow-owned status or outputDepends on workflowWorkflow-defined
systemOxygen-managed metadata0System-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_typeHolds
textStrings
numericNumbers
booleanTrue/false
jsonbStructured JSON — the default for AI and tool outputs
timestamptzTimestamps 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 is text + a select semantic type.
  • Kind (table above) is the column's behavior — relation is a kind, not a data type. Relation columns are never added with columns add; oxygen tables link --to creates one for you, or oxygen tables relate defines 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> --json

Manual

oxygen columns add <table-id> --label "Notes" --key notes --kind manual --data-type text --json

Formula

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\")"}' \
  --json

Use 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"}}}' \
  --json

oxygen 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?" \
  --json

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

By 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 web

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

DepthWhat one row doesTypical credits / row
quickOne search, then reads the top result, then one answer on the column's tier2.1 on Oxygen Fast
standardAn agent plans a few searches and page reads on Oxygen Fast (default for new columns)5.5
deepMore searches and whole-site subpages on Oxygen Balanced10

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

Standard 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"}}' --json

The 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." \
  --json

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

Replace 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"}}}' \
  --json

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

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

The 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 element

Three 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 in inputMapping — actually delivers the cell.
  • A JSON cell interpolates as JSON. {{page_content}} on a jsonb column 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, with invalid_prompt_column and the near-miss keys. The web editor saves anyway — it autosaves while you type — but paints the chip red and columns run refuses 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}}." \
  --json

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

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

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

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

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

Discarding cached cells is specific to lookup: on a manual or source column, oxygen columns retype coerces every stored value instead of dropping it.

  • Cells — per-cell state model.
  • Relations — relation columns and CRM relationships.
  • Waterfalls — provider fallback for common enrichment jobs.

On this page