Resources
Source of truth
This is the hubspot agent CLI; the hs developer CLI (@hubspot/cli) is a different tool and does not manage CRM data or workflows. hubspot <command> --help is authoritative. If anything in this file contradicts --help, trust --help and tell the user. Run hubspot objects types once at the start of a session to see what object types exist in this portal (standard + custom).
Submit Feedback
Use the hubspot feedback command to send a message to the owners of this CLI tool. Pass --source agent so it's attributed to agent traffic (it defaults to user):
This can be anything from:
- Specific bugs and hiccups you encountered
- Things you wish you knew before using the CLI
- Anything your user got confused, frustrated, or upset about
- Anything the user asked for that you couldn't do
- Any tools, capabilities, or skills you wish existed that would make future tasks easier
It takes one short line, attaches to the active HubSpot account, and doesn't block the task — send it and keep going.
Output shape
Every read command (list, search, get) emits JSONL — one JSON object per line:
--properties email,firstname limits which fields the server returns under .properties. Downstream jq should use .properties.email, not .prop_email.
Write commands (create, update, upsert, delete, merge, associations create) accept JSONL on stdin and emit JSONL — one result per input line: {"id":"123","ok":true,"data":{...}} or {"id":"123","ok":false,"error":{"status":...,"message":"..."}}. Order of results matches input order — safe to join by line position.
Read in batch — never one-by-one
The CLI accepts multiple IDs natively. Never pipe IDs into xargs -I{} hubspot objects get ... — that spawns one CLI process per record.
A single hubspot objects get reads up to ~100 IDs per call via the batch endpoint. For more, page in chunks of 100.
Bulk flow: paginate first, then reshape, then write
When operating on all records of a type (or all matches of a filter), always start with pagination-loop.sh — never run a bare list or search to "check how many there are." A bare call returns at most 100 records and you will have to re-fetch them anyway. To size the job first, use hubspot objects count --type <t> [--filter "..."], which returns the total matching count (e.g. {"object_type":"contacts","total":42}) without paging.
The canonical bulk pattern is:
- Paginate all records to a JSONL file
- Reshape with
jqinto the write payload - Pipe to the write command (
update,delete, etc.) with--dry-runfirst
Pagination
list and search return at most 100 records per call. Use resources/pagination-loop.sh to collect all pages into a single JSONL file:
Examples:
The script pages through --after cursors automatically, prints progress to stderr, and writes JSONL to the output file. Run it as a single foreground command — do not background it or reconstruct the loop inline.
Result ordering
search returns newest-first by default — it sorts descending on whichever create-date property the object type defines (createdate for contacts, hs_createdate for activities, and so on) — so the first page holds the most recent records rather than the portal's 2017-era ones. Sort on any other sortable property with --sort <property> (hs_lastmodifieddate, amount, ...) and flip direction with --sort-dir asc. list has no sort control — use search when order matters.
associations list also paginates (--limit / --after, default limit 100): under --format json the next-page cursor is at .meta.next, null once the last page is reached. A record can have far more associated records than one page holds, so treat a present cursor as "more remain" and page with --after until it is null.
Write in batch — always pipe
Write commands accept JSONL on stdin. The transformation between a read shape and a write shape is a jq reshape:
Use plural object names in from/to (contacts:, not contact:).
Safe destructive workflow
Irreversible writes — objects delete, merge, update, upsert, associations delete / limits-update, and metadata deletes (workflows, segments, views, reports, schemas, pipelines, properties) — always require the two-step dry-run → digest → confirm flow, at ANY row count (one record needs it just as much as 10k). Soft writes (objects create, views create/update/replace-field, label-only properties update, segments members-add) run without a digest; their --dry-run is a plain preview.
1. Dry-run — emits ONE preview line for the whole invocation (not per record), at every size:
mutation_kind is RecordMutation for ≤100 rows and BulkData above the bulk threshold — the digest is present in both cases, so never filter on mutation_kind. Confirm values by command: objects delete/update → the record ID (single) or the row count (batch of 2+); objects upsert → the row count (always); objects merge → the secondary ID (one pair) or the row count (batch); metadata deletes → the target's name (workflow/view/report/schema/pipeline), the property/option name, or the row count (batch archives, associations batch ops). Don't construct the confirm value — copy it from apply_command_hint (or .target.id / .target.name on the preview line).
2. Execute within 5 minutes (digest TTL 300s): re-pipe the SAME inputs plus --digest and --confirm:
Executing without --digest fails with digest_required (the error's next_step names the dry-run command); a wrong confirm fails with confirm_mismatch; an expired digest with digest_expired. --force is deprecated and ignored.
Recovery via hubspot history
Every destructive op (and its dry-run) is logged locally. Check what happened in the last hour and what's reversible:
history does not currently restore records — it's an audit log. If you deleted something by mistake, capture the history line and tell the user to restore via the UI.
For CRM property source history (who or what changed a property — WORKFLOW, INTEGRATION, IMPORT, CRM_UI), use hubspot objects history --type <t> --properties <p>. It flattens each property's version history into one row per change; UI-driven (CRM_UI) changes are excluded by default (--include-ui to keep them). Add --id <recordId> to read one record, or omit it to scan a page. This is separate from the local hubspot history audit log above — use it to investigate why a property changed after a bulk op.
Upsert beats search-then-create
For "create if missing, update if present" (the enrichment pattern), use upsert — one CLI call per record, no race condition:
Rate-limit hygiene
create, update, and upsert batch stdin into chunks of up to 100 and call the real batch/create / batch/update / batch/upsert endpoints — one API call per 100 lines, not per line. delete issues one API call per stdin line. Test with head -n 50 before piping a 50k-row file — or use hubspot imports for purpose-built bulk ingest. If the API starts 429ing, the per-line (or per-chunk) output will show {"ok":false,"error":{"status":429,...}} — split your input file and retry the failed lines.
For large CSV ingests, hubspot imports is the purpose-built path: imports start (from a CSV file + import-request JSON; supports --dry-run), imports list, imports get <id>, imports cancel <id> (irreversible — dry-run first, then --digest/--confirm with the import ID), and imports errors <id>. Run hubspot imports --help for the request shape.
Common reshapes
See resources/json-patterns.md for the full set. The two you need 90% of the time:
Known constraints
objects deleteworks under both user-OAuth (browser login, with the object's write scope) and a service key; a 403 means the active token is missing that write scope. The exception is the--gdprpermanent purge, which requires a service key (HUBSPOT_ACCESS_TOKEN) — the GDPR endpoint does not accept user OAuth tokens.objects update/merge/upsertalso accept either token type. Some destructive operations (e.g.associationscreate/delete/batch/labels/limits,schemas delete) remain service-key-only and are enforced server-side, soHUBSPOT_SKIP_AUTH_CHECKwill not get a user token past them.hubspot owners listreturns CRM users; there is noteamsobject. For team-level operations, group byhubspot_owner_idclient-side.hubspot segmentsprovides CRM lists (Lists API):list,get,create,update(metadata),update-filters,delete,restore, andmembers-list/members-add/members-remove.hubspot sequencesprovides read-only access to Sales Hub sequences:list --user-id <id>(paginated,--namefilter),get <id> --user-id <id>(steps + settings), andenrollments <contact_id>(a contact's enrollment history). Sequences are a product API surface (Sales Hub Professional+,automation.sequences.readscope), not a CRM object type —objects list --type sequencesdoes not work, and there is no create/update/delete/enroll.- Maintaining this section: the command surface grows — do not assume an API is absent because it was when this was written. When a new command family ships (see
CHANGELOG.md), revisit these constraints.hubspot --helpis authoritative for what exists today.

