bulk-operations

Foundation patterns for the `hubspot` CLI — JSONL piping, batch read, pagination, dry-run/digest/confirm for destructive ops, and `hubspot history` for recovery. Every other skill builds on this one.

By hubspot · 1,372 installs

npx skills add hubspot/agent-cli-skills --skill bulk-operations

Source repository · Upstream listing

Resources File When to use resources/json patterns.md Reshape patterns for turning a read into an update payload, a search into a delete list, a CSV into an upsert stream. 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: 1. Paginate all records to a JSONL file 2. Reshape with jq into the write payload 3. Pipe to the write command ( update , delete , etc.) with dry run first 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: Write command Required per line shape objects create {"properties":{"field":"value"}} objects update {"id":"123","properties":{"field":"value"}} objects upsert {"idProperty":"email","id":"jane@example.com","properties":{...}} (or use id property email once) objects delete {"id":"123"} objects merge {"primary":"123","secondary":"456"} associations create {"from":"contacts:123","to":"companies:456"} 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 delete works 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 gdpr permanent purge, which requires a service key ( HUBSPOT ACCESS TOKEN ) — the GDPR endpoint does not accept user OAuth tokens. objects update / merge / upsert also accept either token type. Some destructive operations (e.g. associations create/delete/batch/labels/limits, schemas delete ) remain service key only and are enforced server side, so HUBSPOT SKIP AUTH CHECK will not get a user token past them. hubspot owners list returns CRM users; there is no teams object. For team level operations, group by hubspot owner id client side. hubspot segments provides CRM lists (Lists API): list , get , create , update (metadata), update filters , delete , restore , and members list / members add / members remove . hubspot sequences provides read only access to Sales Hub sequences: list user id <id (paginated, name filter), get <id user id <id (steps + settings), and enrollments <contact id (a contact's enrollment history). Sequences are a product API surface (Sales Hub Professional+, automation.sequences.read scope), not a CRM object type — objects list type sequences does 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 help is authoritative for what exists today.