Data Enrichment

hubspot/agent-cli-skills/data-enrichment

by hubspota8eea0880838No license27 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 7 days ago

Match external CSV/JSONL records to CRM contacts (by email) or companies (by domain) and write enriched data back in one pass using `hubspot objects upsert`.

Instructions onlyData & Analytics
AI-generated overview

Upserts CSV/JSONL records into HubSpot contacts or companies by email or domain, with dry-run preview and error handling.

What it does
This skill describes a workflow for matching external CSV or JSONL records to HubSpot CRM contacts by email or companies by domain, then writing the enriched data back in a single upsert pass. It covers reshaping input with jq, previewing with a dry run, lifting the digest and confirm values, executing the upsert, and splitting per-record success and error output. It also documents a read-only alternative using OR-search filters, including the five-filter-group cap, and notes destructive-operation safety practices.
When to use it
Use it when you need to create or update many CRM records from an external spreadsheet or JSONL file keyed by a natural identifier such as email or domain. It is also relevant when you only want to read matching CRM records without writing back, or when you need to inspect and retry failed upsert rows.
Requirements
Requires the hubspot CLI and jq, plus csvkit or another CSV-to-JSONL tool for CSV input. It depends on the bulk-operations skill for JSONL piping, dry-run/digest, history, and rate-limit guidance, and needs HubSpot credentials and network access. It ships no scripts; it is instructions only.

Prereq: read bulk-operations/SKILL.md first — JSONL piping, dry-run/digest, history, and rate-limit hygiene live there. This skill is the upsert-by-natural-key workflow on top.

The core move: upsert, not search-then-create

hubspot objects upsert --type X --id-property <natural-key> reads JSONL on stdin and creates-or-updates each row in one CLI call (the CLI batches 100 rows per API request), keyed by a property (email for contacts, domain for companies). No race window, no branching. Do not loop search → empty? → create.

Per line in: {"id":"[email protected]","properties":{"firstname":"Jane","jobtitle":"VP"}} Per line out: {"id":"123","ok":true,"data":{...}} or {"ok":false,"error":{...}}. Order matches input. The CLI adds no fields of its own — data is the raw batch-upsert API result row.

CSV/JSONL → upsert stream

Reshape with jq, preview with --dry-run, then execute. upsert is irreversible, so the execute step re-pipes the SAME inputs plus the --digest/--confirm lifted from the preview line (upsert confirm = the row count, always). Always lowercase the natural key — CRM match is exact. Confirm available property names with hubspot properties list --type contacts; never hard-code a list. See bulk-operations/resources/json-patterns.md for reshape idioms.

bash
# CSV → JSONL (any tool); example using csvkitcsvjson external.csv | jq -c '.[]' > external.jsonl
# Previewcat external.jsonl \| jq -c '{id:(.email|ascii_downcase), properties:{firstname:.first, lastname:.last, jobtitle:.title, company:.company}}' \| hubspot objects upsert --type contacts --id-property email --dry-run \| tee /tmp/upsert.preview.jsonl
# Lift the digest + confirm (present at every row count; upsert confirm = the row count)digest=$(jq -r 'select(.digest != null) | .digest' /tmp/upsert.preview.jsonl)confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/upsert.preview.jsonl)
# Execute — same pipeline, plus --digest/--confirm, capture resultscat external.jsonl \| jq -c '{id:(.email|ascii_downcase), properties:{firstname:.first, lastname:.last, jobtitle:.title, company:.company}}' \| hubspot objects upsert --type contacts --id-property email --digest "$digest" --confirm "$confirm" \| tee /tmp/upsert.results.jsonl

Companies: swap --type companies --id-property domain and reshape with .domain|ascii_downcase as id.

Handle per-record OK / error output

Split with jq, inspect failure modes, retry just the failures after fixing the inputs:

bash
jq -c 'select(.ok==true)'  /tmp/upsert.results.jsonl > /tmp/upsert.ok.jsonljq -c 'select(.ok==false)' /tmp/upsert.results.jsonl > /tmp/upsert.failed.jsonljq -r '.error.status' /tmp/upsert.failed.jsonl | sort | uniq -c   # status → count

The CLI does not tag rows as created-vs-updated. If the batch-upsert API result row carries a new boolean, jq -r '.data.new' /tmp/upsert.ok.jsonl | sort | uniq -c splits them; otherwise compare .data.createdAt against .data.updatedAt.

429s: split the input and rerun smaller chunks (see bulk-operations rate-limit notes). 400s usually mean a bad property name or invalid enum value — fix the reshape, rerun the failed inputs.

Destructive-op safety

upsert itself is non-destructive, but write-back can clobber populated fields. Always --dry-run first and spot-check. For bulk delete or overwrite of existing data, follow the dry-run → digest → confirm flow in bulk-operations/SKILL.md. Recovery: hubspot history --since 1h.

Match without upsert: OR-search → update

When you only want to read matches (no write-back), or the natural key isn't a CRM property, use repeated --filter flags — each flag is one OR group.

Verified cap: 5 OR groups per call. 6+ returns 400 too many filterGroups (count: N, max allowed: 5). Chunk 5 at a time:

bash
# emails.txt: one lowercased email per linexargs -n5 < emails.txt | while read -r e1 e2 e3 e4 e5; do  args=()  for e in "$e1" "$e2" "$e3" "$e4" "$e5"; do [ -n "$e" ] && args+=(--filter "email=$e"); done  hubspot objects search --type contacts "${args[@]}" --properties email,firstname,companydone > /tmp/matches.jsonl
jq -c '{id, properties:{lifecyclestage:"marketingqualifiedlead"}}' /tmp/matches.jsonl \| hubspot objects update --type contacts --dry-run

For larger keyed enrichments, prefer upsert — one pipeline, no chunking math.

Source and attribution

Source:hubspot/agent-cli-skillsindata-enrichmentat commita8eea08

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal