OXYGENOxygen/ Docs
Guides

Track deals in the CRM

Keep companies, people, and deals as canonical records — open them in the app, write them from the CLI, enrich them, merge duplicates, and promote table columns onto them.

The CRM holds one canonical row per company, person, and deal, each with identities, an owner, a stage, and a timeline.

Records are truth, Tables are work

A table builds and enriches a list: a TAM pull, a CSV, a scrape. A record is that entity deduped, addressed by identity — companies by domain, people by email, deals by deal_key. They connect one way: bind rows to records, then promote the columns worth keeping.

# 1. Resolve each row to a CRM record. Free. In the app: "Add column" → "Bind to CRM record".
oxygen columns add my-leads --label "CRM person" --bind-object people --bind-map email=work_email
oxygen columns run my-leads crm_person --limit 25

# 2. Preview which record fields would change. Free, writes nothing.
oxygen tables promote my-leads --object people --map job_title=job_title,seniority=seniority

# 3. The same command plus --approved writes them onto the records.

columns add prints the key it derived from the label — pass that to columns run. promote defaults to --policy fill-empty-only (also overwrite, skip-conflicts), never touches identity or system attributes, and queues a background run you follow with oxygen table-runs wait <run_id>.

Set up the objects

Sidebar Data → CRM; each object is a child: Companies, People, Deals, plus any custom object. An empty workspace lands on No CRM objects yet.

oxygen crm setup          # dry run: what it would create or repair
oxygen crm setup --live   # creates companies, people, deals

Most crm writes preview first and need --live: setup, assert, activity log|note, objects create|delete|add-attr, relationships define|upsert, merge, automation set, and sync import|configure|run. crm tag, every enrichment verb, and sync enable|disable write on the spot. Check any command's own --help before running it.

Custom objects come from oxygen crm objects create --slug projects --display-name Projects --columns-json '[…]' --live or the form at /crm/objects/new. The slugs companies, people, deals, lists, and activities are reserved. oxygen crm objects lists what exists.

Work a record

/crm/deals opens on a seeded board view named Pipeline, grouped by Pipeline Stage: Discovery → Qualified → Proposal → Negotiation → Won / Lost. Drag a card to move the deal.

Any cell — or the Open record button on row hover — opens the account view: tabs Overview and Activity, plus Conversations / Sequences where the object has them and Notes once it has a markdown column, with the record's attributes down the left. The stage cell opens its picker in place instead. To log activity, click Log a note, call, or meeting…, pick Note, Call, or Meeting, and press Log — internal and free, no preview, no approval.

From the CLI:

oxygen crm search "acme.com" --object companies
oxygen crm get companies <row_id> --json
oxygen crm assert companies --identity domain=acme.com --values-json '{"lifecycle_stage":"customer"}' --live
oxygen crm activity note companies <row_id> "Intro call booked" --live
oxygen crm activity log deals <row_id> --type meeting_booked --summary "Demo with CTO" --live
oxygen crm activity timeline companies acme.com
oxygen crm tag companies <row_id> --add tier-1
oxygen crm pipeline --pipeline sales

search takes exactly one query — narrow it with --object, never a second argument. timeline accepts a row id or an identity value (acme.com, sarah@acme.com); a miss returns crm_record_not_found naming what to pass instead. crm tag writes deltas (--add unions, --remove subtracts) against the same tag vocabulary as sequences and conversations, and needs the tags attribute crm setup adds.

A live crm assert fires the object's standing automatic enrichment on the record it wrote, spending credits up to that object's per-batch ceiling. Run oxygen crm enrichment list first to see what is armed.

Automatic enrichment (paid)

Every CRM grid's top bar carries the enrichment popover, headed New records added to Companies are enriched automatically. It lists each preset's per-record credit cost, the per-batch ceiling, and Backfill existing records with an Enrich … now button and Enrich all.

oxygen crm enrichment list --object companies                       # armed presets, cost per record, ceiling, how many records were never enriched. Free.
oxygen crm enrichment enable company_linkedin_profile --object companies
oxygen crm enrichment backfill --object companies                   # free preview: one batch's cost, and the cost to finish
oxygen crm enrichment backfill --object companies --limit 200 --live --approved --max-credits 2000

Arming covers only records written after it, so existing records stay blank until you backfill. crm setup arms the default presets on objects it creates; crm setup --with-enrichment arms them on objects that already existed. A live batch without --approved fails with approval_required (exit 7). Records past --max-credits are skipped with credit_limit_reached — the preview says how many. oxygen crm enrichment off --object companies clears the configuration entirely.

Merge duplicates

/crm/companies/duplicates (no sidebar entry yet — type the URL) ranks candidate pairs. Review merge opens the field-level diff, Confirm merge writes it.

oxygen crm duplicates companies --limit 25
oxygen crm merge companies <survivor_row_id> <loser_row_id>                     # dry run: field diff + relink plan
oxygen crm merge companies <survivor_row_id> <loser_row_id> --live --confirm

The survivor keeps its values, the loser fills its blanks and is tombstoned with its links moved over. --live without --confirm refuses: "crm merge --live requires --confirm after inspecting the dry-run field diff."

Standing automations

/crm/automation lists the templates that advance a record when a reply, meeting, or signup arrives — crm-lead-stage-router, crm-meeting-booked, crm-signup-to-person — each badged Armed or Disarmed with a toggle. The lead-stage router arms itself the first time a sequence goes live; once disarmed, nothing re-arms it but you.

oxygen crm automation rules
oxygen crm automation set crm-meeting-booked --armed --live --approved --max-credits 1   # drop --live to preview it first
oxygen crm automation audit --limit 20

An armed rule is a standing permission for free internal CRM writes; --max-credits is the ceiling per event delivery.

Two-way sync with HubSpot or Attio

Connect the provider under External → Connections, then:

oxygen crm sync import hubspot --object contacts --into people                             # dry run
oxygen crm sync configure --provider hubspot --object contacts --direction bidirectional   # dry run of the next cycle
oxygen crm sync enable --provider hubspot --object contacts --approved --max-rows 500 --max-credits 1000

sync enable is always live: it arms the cron trigger and the standing write permission, then runs a first cycle. Inbound previews and every cycle read through your own provider credentials, consuming your CRM's API quota. Details in Integrations.

On this page