hubspot-deal-ingest skill

Role: universal HubSpot deal creator. Any source produces a parsedDeal object → this skill handles the rest.

This skill wraps existing code, it doesn’t reimplement it. Canonical implementation lives at /home/opsadmin/.openclaw/workspace/scripts/hubspot-deal-creator.js (1006 lines, 11 exported functions).

⚡ Execution-mode model routing (G-MODEL-ROUTING-AT-EXEC)

This skill is execution-mode by definition. Per CLAUDE.md “MANDATORY: Execution-Mode Model Routing” + feedback_model_routing_at_exec.md:

Step 1 of every invocation: decompose the work and dispatch via the Agent tool with model: "sonnet" using the 8-element prompt contract (task goal, evidence, tool to use, test requirements, acceptance criteria, rollback command, rule constraints, output format).

Opus (main conversation) role = dispatcher + verifier + Henry-gate. Direct Opus script invocation / code edits = governance violation unless the work fails the Sonnet rubric (novel architecture, rule arbitration, Henry decision gate, first instance of a new pattern). NEVER reimplement createDealFromParsed() — always call it via the canonical script. Verify each subagent report against acceptance criteria before marking complete.

Architecture

[any source: IL, Crexi, PropStream, FB, manual]
        │
        ▼
adapter (per-source) → produces a parsedDeal object (see PARSED-DEAL-CONTRACT.md)
        │
        ▼
hubspot-deal-creator.js → createDealFromParsed(parsedDeal)
        │
        ├─→ dedup search (by source+source_id, then address)
        ├─→ contact find-or-create (by phone, last 10 digits)
        ├─→ create acquisition deal (pipeline 877963314)
        ├─→ create dispo deal (pipeline 816046)
        ├─→ associate contact ↔ both deals
        └─→ associate acq ↔ dispo (type 36)
        │
        ▼
HubSpot CRM (portal 6193101)

Pipelines + stages

PipelineIDOwnerFirst stageUse
Acquisition877963314aurora@reri.cosource-specific (e.g. 1320086154 for IL, 1320111522 for OpenPhone)CH-side (where deal originates)
Wholesale/Dispo816046aurora@reri.co1006885379 (New Property)Buyer-side (when ready to dispo)

Source-stage mapping (from hubspot-deal-creator.js line 64):

SourceFirst stage
investorliftnew_deal_investorlift
facebooknew_deal_facebook
connected_investorsnew_deal_connected_investors
asset_columnnew_deal_asset_column
salesmsgnew_deal_salesmsg
openphone_inbound / openphonenew_deal_openphone
referral / cold_outreach / emailnew_deal_email

Capabilities (entry points)

The skill exposes createDealFromParsed() from hubspot-deal-creator.js. Callers (adapters) invoke it like:

const { createDealFromParsed } = require('/home/opsadmin/.openclaw/workspace/scripts/hubspot-deal-creator.js');
 
const parsedDeal = {
  // see docs/PARSED-DEAL-CONTRACT.md for full shape
  address: '123 Main St, Phoenix, AZ 85001',
  askingPrice: '250000',
  arv: '380000',
  beds: 3, baths: 2, sqft: 1450,
  wholesalerName: 'ACME Wholesalers LLC',
  sourcePhone: '+15551234567',
  dealSource: 'investorlift',
  // ... etc
};
 
const result = await createDealFromParsed(parsedDeal);
// result = { deal, contact, isNewContact, isDuplicate, dispoDeal }

For the IL adapter specifically, see /home/opsadmin/.openclaw/workspace/scripts/workers/acq-deals-hs-syncer.js — it reads from acquisition_deals Supabase, transforms via supabaseDealToParsed(), then calls createDealFromParsed().

Custom property catalog (HubSpot side)

FamilyCountPrefixOwnerSource
Standard HS8(no prefix)HubSpotdealname, amount, dealtype, pipeline, dealstage, hubspot_owner_id, closedate, description
RERI custom14reri_*RERIdispo_price, contract_holder_price, amount_spread, arv_flag, showing_*, delivered_vacant, deal_type, etc.
InvestorLift38investorlift_*RERIprice, arv, contact_*, tenant_status, motivation_level, address fields, etc.
Property~10property_*HubSpot templatebeds, baths, sqft, year_built, lot_size, occupancy, condition, photos
Inbound source tracking5(mixed)RERIinbound_sender_phone, inbound_message_text, inbound_source, raw_message, prequalify_score

Full catalog in workspace-acquisitions/FIELDS.md and agents/{acquisitions,atlas}/agent/FIELDS.md.

Triggers

User says any of:

  • “push this deal to HubSpot”
  • “create HS deal from
  • “sync deal candidate”
  • “add to acquisition pipeline”
  • “create both acq and dispo deals for X”

For source-specific bulk syncs (e.g., “sync all IL deals to HubSpot”), route to the source’s adapter worker (acq-deals-hs-syncer.js for IL), not directly to this skill — the adapter handles pagination + Supabase state tracking.

Capabilities NOT in this skill

CapabilityLives in
Source data ingestionper-source skill (e.g., il-marketplace-pull for IL)
Deal qualification / 5-gate computeacquisitions-outreach skill (gate-computer.js)
Initial outbound SMSacquisitions-outreach skill
Follow-up cadenceacquisitions-followup skill
Dispo blast to buyersdispo-blast skill
Stage transitions on conversationacq-deals-hs-syncer.js workers + stage-sync.js

Failure modes

SymptomCauseRecovery
HUBSPOT_PAT missingenv not loadedset -a; source /home/opsadmin/.openclaw/master.env; set +a
429 from HubSpotrate limit (100 req/10s; each create ≈ 6 requests)adapter must throttle to ≥500ms/deal
Deal created but no contactparsedDeal.sourcePhone was nulladapters must populate sourcePhone OR skip contact creation
isDuplicate: truededup hit on source+source_id or addressresult.deal points at existing deal; update it instead of creating
Dispo deal failed but acq creatednon-fatal in current code (logs warning)re-run sync; findExistingDeal will find acq, only create dispo

LLM extraction → HS field mapping (new 2026-05-03)

When acquisition_deals has LLM-extracted data (from enrich-descriptions-llm.js), the sync pipeline maps it to HubSpot in three tiers:

Tier A — Existing HS props that get AUTO-FILLED (~22 props)

These properties already exist in HubSpot. The adapter writes LLM-extracted values to them only when blank (never overwrites human-edited fields). Always updates investorlift_scraped_at timestamp.

HS propLLM sourceOverwrite policy
investorlift_motivation_levelmotivation.levelFill blank only
investorlift_motivation_urgencyurgency_signals[] countFill blank only
investorlift_seller_motivationmotivation.reasons[].join(', ')Fill blank only
investorlift_property_conditioncondition_gradeFill blank only
investorlift_condition_confidenceextraction confidence per clusterFill blank only
investorlift_tenant_statustenant.lease_type or tenant.occupancyFill blank only
investorlift_tenant_confidenceextraction confidenceFill blank only
investorlift_transaction_typedeal_structureFill blank only
investorlift_transaction_confidenceextraction confidenceFill blank only
investorlift_quality_score / _data_quality_scorequality_scoreFill blank only
investorlift_deal_summarypitch_hook (one-liner)Fill blank only
investorlift_overview_summaryLLM-summarized description (~200 chars)Fill blank only
investorlift_analysis_feedbackrisks[].join('; ')Fill blank only
investorlift_feedback_notesopportunities[].join('; ')Fill blank only
investorlift_contact_1_name/phone/emailprimary contact per precedence ruleFill blank only
investorlift_contact_2_*secondary contact (if precedence rule produces one)Fill blank only
investorlift_contact_countcomputedAlways update
investorlift_holder_companycontact.companyFill blank only
investorlift_picture_linkspicture_links[]Fill blank only
reri_delivered_vacanttenant.delivery_vacantFill blank only
reri_deal_typemaps from deal_structureFill blank only
reri_st_nameparsed from validated addressFill blank only
reri_internal_showing_instructionsaccess.window_textFill blank only
reri_showing_datetimeshowing.datetimeFill blank only

Tier B — NEW HS props to CREATE (14 props)

Henry approved 2026-05-03. These require a one-time HubSpot Properties API call (CREATE-HS-PROPS-SPEC.json). Until created, values are written to acquisition_deals.meta JSONB and surfaced via Acquisitions Control Center.

New HS propTypeLLM sourceWhy team needs it
investorlift_flood_zoneenum (X/A/AE/VE/unknown)flood_zoneBuyer-financing filter
investorlift_fema_50_50boolfema_50_50_flagHard kill flag for retail flips
investorlift_title_statusenum (clean/probate/trust/lien/tax/unknown)title_statusPipeline routing
investorlift_deal_structure_validatedenum (assignment/double_close/novation/sub2/seller_finance/unknown)deal_structureWorkflow trigger; validates against existing _transaction_type
investorlift_cash_onlyboolterms.cash_onlyBuyer pool filter
investorlift_emd_amountnumberterms.emd_amountFinance check
investorlift_emd_refundableenum (yes/no/conditional/unknown)terms.emd_termsRisk filter
investorlift_close_timeline_daysnumberterms.close_timeline_daysSchedule planning
investorlift_ideal_strategymulti-checkbox (flip/hold_rental/brrrr/airbnb/midterm/land_dev)ideal_strategy[]Buyer-match scoring
investorlift_neighborhood_classenum (A/B/C/D/unknown)neighborhood_classBuyer cohort filter
investorlift_deal_kill_flagsmulti-checkbox (flood/structural/title/fema/contamination/age_restricted/none)deal_kill_flags[]Hard triage filter
investorlift_pii_detectedboolpii_detectedTriggers dispo-blast PII scrub workflow
investorlift_extraction_confidencenumber 1-10confidence_in_extractionQuality gate for downstream automation
investorlift_gdrive_folder_urlstring/URLGDrive folder creation scriptAsset linkage in ACC (mirrors DCC pattern)

Tier C — JSONB-only fields (~55 fields)

Never go to HubSpot. Stay in data_il_marketplace.llm_extracted JSONB. Surfaced via Acquisitions Control Center.

Includes: roof_year, hvac_year, foundation_type, foundation_issues, monthly_rent_current/market, comps_provided[], sub2_terms blob, special_features, urgency_signals, motivation_reasons, risks[], opportunities[], action_items_for_acq[], action_items_for_dispo[], missing_critical_fields[], rental_potential_text, ch_negotiation_leverage[], showing_window_text, lease_end_date, etc.

Full taxonomy: docs/LLM-EXTRACTION-TAXONOMY.md (in il-marketplace-pull skill).

Contact precedence rule (Henry directive 2026-04-28)

When extracting contacts, follow this matrix:

Description contactAPI dispositions_managerPrimarySecondarycontact_role
Has name + phoneDifferent nameDescriptionAPIcontract_holder
Has name + phoneSame nameEither (matched)nonecontract_holder
No nameHas nameAPInonecontract_holder (assumed)
Both emptyBoth emptynonenoneflag missing_contact

Why: API dispositions_manager is often marketing/admin. Description contact is the operational CH. Hanford 311765: API=Raymond, desc=Sahil.

Integration points:

  1. LLM extraction prompt — instructs LLM to extract from description, post-processor reconciles against API
  2. HubSpot field mapping — investorlift_contact_1_* = primary; _contact_2_* = secondary; dealname reads primary first name
  3. Aurora outreach — addresses primary contact_first_name only

Open decisions / future expansion

These are documented in project_acquisition_deals_centralized_schema.md:

  • 8 unmapped IL detail fields worth wiring as new HS custom properties: matterport_url, zestimate, roof_age, foundation_condition, heating_system_age, hunter_email_score, hunter_linkedin_url, apollo_employee_count. Requires HS admin to create the properties first; deferred until consolidation phases 1-3 confirmed via Kimi audit.
  • Owner assignment policy — currently aurora@reri.co for all deals. Future: route by state, or to per-source “Inbox” owner.
  • Re-ingest cadence — currently overwrites all fields on every run via upsert. Need to confirm whether human-edited fields should be preserved.

Reference docs (in this skill)

  • workspace/scripts/hubspot-deal-creator.js — canonical creator (1006 lines)
  • workspace/scripts/workers/acq-deals-hs-syncer.js — IL adapter (334 lines)
  • workspace-acquisitions/FIELDS.md — RERI + IL custom property catalog
  • agents/acquisitions/agent/FIELDS.md — same content, agent-side reference